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.
This commit is contained in:
evazion
2021-11-25 15:58:35 -06:00
parent 5dc67613e6
commit 0baca68a37
6 changed files with 44 additions and 30 deletions

View File

@@ -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|

View File

@@ -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"))

View File

@@ -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?

View File

@@ -36,7 +36,5 @@
<% end %>
<% end %>
<% unless post_set.is_random? %>
<%= numbered_paginator(post_set.posts) %>
<% end %>
<%= numbered_paginator(post_set.posts) %>
</div>

View File

@@ -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

View File

@@ -1099,6 +1099,14 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match([], "exif:DNE")
end
should "return posts for the random:<N> 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)