From 0baca68a377eeb6b9d42bdaa640478016c24f8c8 Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 25 Nov 2021 15:58:35 -0600 Subject: [PATCH] search: make `order:random` truly random; add `random:N` metatag. Make the `order:random` metatag truly randomize the search. Add a `random:N` metatag that returns up to N random posts, like what `order:random` did before. `order:random` now returns the entire search in random order. Before it just returned a pageful of pseudorandom posts. This will be more accurate for small searches, but slower for large searches. If `order:random` times out, try `random:N` instead. The `random:N` metatag returns up to N pseudorandom posts. This is faster than `order:random` for large searches, but for small searches, it may return less than N posts, and the randomness may be biased. Some posts may be more likely than others to appear. N must be between 0 and 200. Also, `/posts?tags=touhou&random=1` now redirects to `/posts?tags=touhou+random:N`. Before the `random=1` param acted like a free `order:random` tag; now it redirects to a `random:N` search, so it counts against your tag limit. --- app/controllers/posts_controller.rb | 6 ++++- app/logical/post_query_builder.rb | 12 +++++++++- app/logical/post_sets/post.rb | 23 ++++--------------- .../posts/partials/index/_posts.html.erb | 4 +--- test/functional/posts_controller_test.rb | 15 +++++++----- test/unit/post_query_builder_test.rb | 14 +++++++++++ 6 files changed, 44 insertions(+), 30 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 7ed34a00e..4c6c1ba39 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -8,9 +8,13 @@ class PostsController < ApplicationController respond_with(@post) do |format| format.html { redirect_to(@post) } end + elsif params[:random].to_s.truthy? + post_set = PostSets::Post.new(params[:tags], params[:page], params[:limit], format: request.format.symbol, view: params[:view]) + query = "#{post_set.normalized_query.to_s} random:#{post_set.per_page}".strip + redirect_to posts_path(tags: query, page: params[:page], limit: params[:limit], format: request.format.symbol, view: params[:view]) else tag_query = params[:tags] || params.dig(:post, :tags) - @post_set = PostSets::Post.new(tag_query, params[:page], params[:limit], random: params[:random], format: params[:format], view: params[:view]) + @post_set = PostSets::Post.new(tag_query, params[:page], params[:limit], format: request.format.symbol, view: params[:view]) @posts = authorize @post_set.posts, policy_class: PostPolicy @post_set.log! respond_with(@posts) do |format| diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb index e9ebf7c44..2e00515ce 100644 --- a/app/logical/post_query_builder.rb +++ b/app/logical/post_query_builder.rb @@ -38,7 +38,7 @@ class PostQueryBuilder ordpool note comment commentary id rating source status filetype disapproved parent child search embedded md5 width height mpixels ratio score upvotes downvotes favcount filesize date age order limit tagcount pixiv_id pixiv - unaliased exif duration + unaliased exif duration random ] + COUNT_METATAGS + COUNT_METATAG_SYNONYMS + CATEGORY_COUNT_METATAGS ORDER_METATAGS = %w[ @@ -218,6 +218,8 @@ class PostQueryBuilder user_subquery_matches(PostVote.active.positive.visible(current_user), value, field: :user) when "downvoter", "downvote" user_subquery_matches(PostVote.active.negative.visible(current_user), value, field: :user) + when "random" + Post.all # handled in the `build` method when *CATEGORY_COUNT_METATAGS short_category = name.delete_suffix("tags") category = TagCategory.short_name_mapping[short_category] @@ -513,6 +515,11 @@ class PostQueryBuilder relation = search_order(relation, find_metatag(:order)) end + if count = find_metatag(:random) + count = Integer(count).clamp(0, PostSets::Post::MAX_PER_PAGE) + relation = relation.random(count) + end + relation end @@ -688,6 +695,9 @@ class PostQueryBuilder when /(#{TagCategory.short_name_list.join("|")})tags_asc/ relation = relation.order("posts.tag_count_#{TagCategory.short_name_mapping[$1]} ASC") + when "random" + relation = relation.order("random()") + when "rank" relation = relation.where("posts.score > 0 and posts.created_at >= ?", 2.days.ago) relation = relation.order(Arel.sql("log(3, posts.score) + (extract(epoch from posts.created_at) - extract(epoch from timestamp '2005-05-24')) / 35000 DESC")) diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb index 34f576e93..a57b8c78b 100644 --- a/app/logical/post_sets/post.rb +++ b/app/logical/post_sets/post.rb @@ -7,16 +7,15 @@ module PostSets MAX_PER_PAGE = 200 MAX_SIDEBAR_TAGS = 25 - attr_reader :page, :random, :format, :tag_string, :query, :normalized_query, :view + attr_reader :page, :format, :tag_string, :query, :normalized_query, :view delegate :post_count, to: :normalized_query - def initialize(tags, page = 1, per_page = nil, user: CurrentUser.user, random: false, format: "html", view: "simple") + def initialize(tags, page = 1, per_page = nil, user: CurrentUser.user, format: "html", view: "simple") @query = PostQueryBuilder.new(tags, user, tag_limit: user.tag_query_limit, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?) @normalized_query = query.normalized_query @tag_string = tags @page = page @per_page = per_page - @random = random.to_s.truthy? @format = format.to_s @view = view.presence || "simple" end @@ -92,25 +91,11 @@ module PostSets end def max_per_page - (format == "sitemap") ? 10_000 : MAX_PER_PAGE - end - - def is_random? - random || query.find_metatag(:order) == "random" - end - - def get_random_posts - ::Post.user_tag_match(tag_string).random(per_page) + (format.to_sym == :sitemap) ? 10_000 : MAX_PER_PAGE end def posts - @posts ||= begin - if is_random? - get_random_posts.paginate(page, search_count: false, limit: per_page, max_limit: max_per_page).load - else - normalized_query.paginated_posts(page, includes: includes, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load - end - end + @posts ||= normalized_query.paginated_posts(page, includes: includes, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load end def hide_from_crawler? diff --git a/app/views/posts/partials/index/_posts.html.erb b/app/views/posts/partials/index/_posts.html.erb index 5bbf4ecbc..e8956443a 100644 --- a/app/views/posts/partials/index/_posts.html.erb +++ b/app/views/posts/partials/index/_posts.html.erb @@ -36,7 +36,5 @@ <% end %> <% end %> - <% unless post_set.is_random? %> - <%= numbered_paginator(post_set.posts) %> - <% end %> + <%= numbered_paginator(post_set.posts) %> diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb index 491274015..107866550 100644 --- a/test/functional/posts_controller_test.rb +++ b/test/functional/posts_controller_test.rb @@ -309,18 +309,21 @@ class PostsControllerTest < ActionDispatch::IntegrationTest get posts_path, params: { tags: "order:random" } assert_response :success - get posts_path, params: { random: "1" } - assert_response :success + get posts_path(random: "1") + assert_redirected_to posts_path(tags: "random:20", format: :html) - get posts_path(format: :json), params: { random: "1" } - assert_response :success + get posts_path(random: "1"), as: :json + assert_redirected_to posts_path(tags: "random:20", format: :json) + + get posts_path(tags: "touhou", random: "true") + assert_redirected_to posts_path(tags: "touhou random:20", format: :html) end should "render with multiple posts" do @posts = create_list(:post, 2) - get posts_path, params: { random: "1" } - assert_response :success + get posts_path(random: "1") + assert_redirected_to posts_path(tags: "random:20", format: :html) end should "return all posts for a .json response" do diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index a0706f543..c48821468 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -1099,6 +1099,14 @@ class PostQueryBuilderTest < ActiveSupport::TestCase assert_tag_match([], "exif:DNE") end + should "return posts for the random: metatag" do + post = create(:post) + + assert_tag_match([], "random:0") + assert_tag_match([post], "random:1") + assert_tag_match([post], "random:1000") + end + should "return posts ordered by a particular attribute" do posts = (1..2).map do |n| tags = ["tagme", "gentag1 gentag2 artist:arttag char:chartag copy:copytag"] @@ -1210,6 +1218,12 @@ class PostQueryBuilderTest < ActiveSupport::TestCase end end + should "return posts for order:random" do + post = create(:post) + + assert_tag_match([post], "order:random") + end + should "return posts for a filesize search" do post = create(:post, file_size: 1.megabyte)