docbrown/app/services/search/postgres/comment.rb
Alex cf0baf05f9
[Search 2.0] Comments on search page (#13401)
* Working MVP

* Add comment to COMMENTABLE_TYPES

* Add specs to search_spec

* Add service specs and fix ordering

* Fixed queries and specs

* Clarify COMMENTABLE_TYPES comment

* Move FORCED_EAGER_LOAD_QUERY to Comment

* Add highlighting to Comment search

* Add explicit .to_s on class_name params

* Remove Comment#forced_eager_load_serialized_data

* Revert CommentSerializer changes

* Hardcode class_name attribute

* Add PR feedback

* Use one liner syntax in serializer

* Update user comment

* Update path attribute logic

* Fix user_ids

* Remove PodcastEpisode skipped spec

* Add sort order to Search::Postgres::Comment

* Fix search_specs

* Move pagination TODO comment
2021-04-20 12:34:21 -04:00

118 lines
4 KiB
Ruby

module Search
module Postgres
class Comment
ATTRIBUTES = [
"COALESCE(articles.published, false) AS commentable_published",
"COALESCE(articles.title, '') AS commentable_title",
"comments.body_markdown",
"comments.commentable_id",
"comments.commentable_type",
"comments.created_at",
"comments.id AS id",
"comments.public_reactions_count",
"comments.score",
"comments.user_id",
].freeze
private_constant :ATTRIBUTES
USER_ATTRIBUTES = %i[
id
name
profile_image
username
].freeze
private_constant :USER_ATTRIBUTES
ARTICLE_COMMENTABLE_QUERY = <<-SQL.freeze
LEFT JOIN articles
ON comments.commentable_id = articles.id
AND comments.commentable_type = 'Article'
SQL
private_constant :ARTICLE_COMMENTABLE_QUERY
DEFAULT_PER_PAGE = 60
private_constant :DEFAULT_PER_PAGE
DEFAULT_SORT_BY = "comments.score".freeze
private_constant :DEFAULT_SORT_BY
DEFAULT_SORT_DIRECTION = :desc
private_constant :DEFAULT_SORT_DIRECTION
MAX_PER_PAGE = 120 # to avoid querying too many items, we set a maximum amount for a page
private_constant :MAX_PER_PAGE
# We filter comments for those that are:
# 1. On Articles
# 2. Not deleted
# 3. Not hidden by commentable user (i.e. an Article author didn't hide the comment)
# 4. Are attached to published articles
QUERY_FILTER = <<-SQL.freeze
comments.commentable_type = 'Article' AND
comments.deleted = false AND
comments.hidden_by_commentable_user = false AND
articles.published = true
SQL
private_constant :QUERY_FILTER
def self.search_documents(
page: 0,
per_page: DEFAULT_PER_PAGE,
sort_by: DEFAULT_SORT_BY,
sort_direction: DEFAULT_SORT_DIRECTION,
term: nil
)
sort_by ||= DEFAULT_SORT_BY
# The UI and serializer rename created_at (the actual DB column name) to
# published_at
sort_by = "comments.created_at" if sort_by == "published_at"
sort_direction ||= DEFAULT_SORT_DIRECTION
# NOTE: [@rhymes/atsmith813] we should eventually update the frontend
# to start from page 1
page = page.to_i + 1
per_page = [(per_page || DEFAULT_PER_PAGE).to_i, MAX_PER_PAGE].min
relation = ::Comment.joins(ARTICLE_COMMENTABLE_QUERY).where(QUERY_FILTER)
relation = relation.search_comments(term).with_pg_search_highlight if term.present?
relation = relation.select(*ATTRIBUTES).reorder("#{sort_by}": sort_direction)
results = relation.page(page).per(per_page)
# NOTE: [@rhymes/atsmith813] an earlier version used `.includes(:user)`
# to preload users, unfortunately it's not possible in Rails to specify
# which fields of the included relation's table to select ahead of time.
# The `users` table is massive (115 columns on March 2021) and thus we
# shouldn't load it all in memory just to select a few fields.
# For these reasons I decided to avoid preloading altogether and issue
# an additional SQL query to load User objects
# (see https://github.com/forem/forem/pull/4744#discussion_r345698674
# and https://github.com/rails/rails/issues/15185#issuecomment-351868335
# for additional context)
user_ids = results.pluck(:user_id)
users = find_users(user_ids)
serialize(results, users)
end
def self.find_users(user_ids)
::User
.where(id: user_ids)
.select(*USER_ATTRIBUTES)
.index_by(&:id)
end
private_class_method :find_users
def self.serialize(results, users)
Search::PostgresCommentSerializer
.new(results, params: { users: users }, is_collection: true)
.serializable_hash[:data]
.pluck(:attributes)
end
private_class_method :serialize
end
end
end