FIX: Select earliest post when aggregating posts in a topic for search.

This is a revert of
d8c796bc44
and
5bf0a0893b.

Linking to the post within a topic that has the highest rank was
confusing users and hard to explain because ranking is determined via
the PG ranking function. See the following meta topics for the
complaints after we switch to the new ordering:

1. https://meta.discourse.org/t/title-search-not-working-as-expected/157737
2. https://meta.discourse.org/t/search-results-should-prioritize-first-post-in-topic-when-title-matches-search-term/175154
This commit is contained in:
Alan Guo Xiang Tan 2021-02-01 13:40:06 +08:00
parent 91fb298510
commit 4b3f65bb26
2 changed files with 49 additions and 87 deletions

View File

@ -986,53 +986,30 @@ class Search
posts
end
if aggregate_search
aggregate_relation = Post.unscoped
.select("subquery.topic_id id")
.group("subquery.topic_id")
posts = posts.select(posts.arel.projections)
end
if @order == :latest
posts = posts.reorder("posts.created_at DESC")
if aggregate_search
aggregate_relation = aggregate_relation
.select(
"MAX(subquery.post_number) post_number",
"MAX(subquery.created_at) created_at"
)
.order("created_at DESC")
posts = posts.order("MAX(posts.created_at) DESC")
else
posts = posts.reorder("posts.created_at DESC")
end
elsif @order == :latest_topic
posts = posts.order("topics.created_at DESC")
if aggregate_search
posts = posts.select("topics.created_at topic_created_at")
aggregate_relation = aggregate_relation
.select(
"MIN(subquery.post_number) post_number",
"MAX(subquery.topic_created_at) topic_created_at"
)
.order("topic_created_at DESC")
posts = posts.order("MAX(topics.created_at) DESC")
else
posts = posts.order("topics.created_at DESC")
end
elsif @order == :views
posts = posts.order("topics.views DESC")
if aggregate_search
posts = posts.select("topics.views topic_views")
aggregate_relation = aggregate_relation
.select(
"MIN(subquery.post_number) post_number",
"MAX(subquery.topic_views) topic_views"
)
.order("topic_views DESC")
posts = posts.order("MAX(topics.views) DESC")
else
posts = posts.order("topics.views DESC")
end
elsif @order == :likes
if aggregate_search
posts = posts.order("MAX(posts.like_count) DESC")
else
posts = posts.order("posts.like_count DESC")
end
else
rank = <<~SQL
TS_RANK_CD(
@ -1069,30 +1046,21 @@ class Search
"(#{rank} * #{category_priority_weights})"
end
posts =
if aggregate_search
posts = posts.select("#{data_ranking} rank", "topics.bumped_at topic_bumped_at")
.order("rank DESC", "topic_bumped_at DESC")
aggregate_relation = aggregate_relation
.select(
"(ARRAY_AGG(subquery.post_number ORDER BY subquery.rank DESC, subquery.topic_bumped_at DESC))[1] post_number",
"MAX(subquery.rank) rank", "MAX(subquery.topic_bumped_at) topic_bumped_at"
)
.order("rank DESC", "topic_bumped_at DESC")
posts.order("MAX(#{data_ranking}) DESC")
else
posts = posts.order("#{data_ranking} DESC", "topics.bumped_at DESC")
end
posts.order("#{data_ranking} DESC")
end
posts = posts.order("topics.bumped_at DESC")
end
posts =
if secure_category_ids.present?
posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted) OR (categories.id IN (?))", secure_category_ids).references(:categories)
posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted) OR (categories.id IN (?))", secure_category_ids).references(:categories)
else
posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted)").references(:categories)
end
if aggregate_search
posts = yield(posts) if block_given?
posts = aggregate_relation.from(posts)
posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted)").references(:categories)
end
if @order
@ -1169,37 +1137,29 @@ class Search
Search.min_post_id
end
min_or_max = @order == :latest ? "max" : "min"
query =
if @order == :likes
# likes are a pain to aggregate so skip
query = posts_query(limit, **default_opts).select('topics.id', 'posts.post_number')
posts_query(limit, type_filter: opts[:type_filter])
.select('topics.id', "posts.post_number")
else
posts_query(limit, aggregate_search: true, type_filter: opts[:type_filter])
.select('topics.id', "#{min_or_max}(posts.post_number) post_number")
.group('topics.id')
end
if min_id > 0
low_set = query.dup.where("post_search_data.post_id < #{min_id}")
high_set = query.where("post_search_data.post_id >= #{min_id}")
{ default: wrap_rows(high_set), remaining: wrap_rows(low_set) }
else
{ default: wrap_rows(query) }
end
else
query = posts_query(limit, **default_opts, aggregate_search: true) do |posts|
if min_id > 0
posts.select("post_search_data.post_id post_search_data_post_id")
else
posts
end
return { default: wrap_rows(high_set), remaining: wrap_rows(low_set) }
end
if min_id > 0
low_set = query.dup.where("subquery.post_search_data_post_id < #{min_id}")
high_set = query.where("subquery.post_search_data_post_id >= #{min_id}")
{ default: wrap_rows(high_set), remaining: wrap_rows(low_set) }
else
# double wrapping so we get correct row numbers
{ default: wrap_rows(query) }
end
end
end
def aggregate_posts(post_sql)
return [] unless post_sql

View File

@ -601,8 +601,8 @@ describe Search do
expect(result.posts.pluck(:id)).to eq([post2.id, post.id])
end
it 'aggregates searches in a topic by returning the post with the highest rank' do
_post = Fabricate(:post, topic: topic, raw: "this is a play post")
it 'aggregates searches in a topic by returning the post with the lowest post number' do
post = Fabricate(:post, topic: topic, raw: "this is a play post")
post2 = Fabricate(:post, topic: topic, raw: "play play playing played play")
post3 = Fabricate(:post, raw: "this is a play post")
@ -613,7 +613,7 @@ describe Search do
results = Search.execute('play')
expect(results.posts.map(&:id)).to eq([
post2.id,
post.id,
post3.id
])
end
@ -1892,9 +1892,11 @@ describe Search do
it 'allows to define custom order' do
expect(Search.new("advanced").execute.posts).to eq([post1, post0])
Search.advanced_order(:chars) do |posts|
posts.reorder("(SELECT LENGTH(raw) FROM posts WHERE posts.topic_id = subquery.topic_id) DESC")
posts.reorder("MAX(LENGTH(posts.raw)) DESC")
end
expect(Search.new("advanced order:chars").execute.posts).to eq([post0, post1])
end
end