Add my reacted statuses search

This commit is contained in:
KMY 2023-09-15 18:50:45 +09:00
parent 7b88422644
commit 5fb6bce744
9 changed files with 124 additions and 16 deletions

View file

@ -157,6 +157,6 @@ class AccountsIndex < Chewy::Index
field(:domain, type: 'keyword', value: ->(account) { account.domain || '' })
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(account) { account.searchable_text })
end
end

View file

@ -149,7 +149,7 @@ class PublicStatusesIndex < Chewy::Index
root date_detection: false do
field(:id, type: 'long')
field(:account_id, type: 'long')
field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'sudachi_analyzer') }
field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(status) { status.searchable_text })
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
field(:language, type: 'keyword')
field(:domain, type: 'keyword', value: ->(status) { status.account.domain || '' })

View file

@ -160,17 +160,26 @@ class StatusesIndex < Chewy::Index
if status.searchability == 'direct'
status.searchable_by.empty?
else
status.searchability == 'limited' ? status.account.domain.present? : false
status.searchability == 'limited' ? !status.local? : false
end
}
root date_detection: false do
field(:id, type: 'long')
field(:account_id, type: 'long')
field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'sudachi_analyzer') }
field(:text, type: 'text', analyzer: 'sudachi_analyzer', value: ->(status) { status.searchable_text })
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
field(:mentioned_by, type: 'long', value: ->(status) { status.mentioned_by })
field(:favourited_by, type: 'long', value: ->(status) { status.favourited_by })
field(:reblogged_by, type: 'long', value: ->(status) { status.reblogged_by })
field(:bookmarked_by, type: 'long', value: ->(status) { status.bookmarked_by })
field(:bookmark_categoried_by, type: 'long', value: ->(status) { status.bookmark_categoried_by })
field(:emoji_reacted_by, type: 'long', value: ->(status) { status.emoji_reacted_by })
field(:referenced_by, type: 'long', value: ->(status) { status.referenced_by })
field(:voted_by, type: 'long', value: ->(status) { status.voted_by })
field(:searchability, type: 'keyword', value: ->(status) { status.compute_searchability })
field(:visibility, type: 'keyword', value: ->(status) { status.searchable_visibility })
field(:language, type: 'keyword')
field(:domain, type: 'keyword', value: ->(status) { status.account.domain || '' })
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })

View file

@ -59,13 +59,15 @@ class Search extends PureComponent {
defaultOptions = [
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
{ label: <><mark>my:</mark> <FormattedList type='disjunction' value={['favourited', 'bookmarked', 'boosted']} /></>, action: e => { e.preventDefault(); this._insertText('my:') } },
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
{ label: <><mark>domain:</mark> <FormattedMessage id='search_popout.domain' defaultMessage='domain' /></>, action: e => { e.preventDefault(); this._insertText('domain:') } },
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } },
{ label: <><mark>order:</mark> <FormattedList type='disjunction' value={['desc', 'asc']} /></>, action: e => { e.preventDefault(); this._insertText('order:') } },
];
setRef = c => {

View file

@ -4,6 +4,7 @@ class SearchQueryTransformer < Parslet::Transform
SUPPORTED_PREFIXES = %w(
has
is
my
language
from
before
@ -11,6 +12,7 @@ class SearchQueryTransformer < Parslet::Transform
during
in
domain
order
).freeze
class Query
@ -34,6 +36,18 @@ class SearchQueryTransformer < Parslet::Transform
search
end
def order_by
return @order_by if @order_by
@order_by = 'desc'
order_clauses.each { |clause| @order_by = clause.term }
@order_by
end
def valid
must_clauses.any? || must_not_clauses.any? || filter_clauses.any?
end
private
def clauses_by_operator
@ -56,6 +70,10 @@ class SearchQueryTransformer < Parslet::Transform
clauses_by_operator.fetch(:filter, [])
end
def order_clauses
clauses_by_operator.fetch(:order, [])
end
def indexes
case @flags['in']
when 'library'
@ -214,6 +232,7 @@ class SearchQueryTransformer < Parslet::Transform
if @term.start_with?('#')
{ match: { tags: { query: @term, operator: 'and' } } }
else
# Memo for checking when manually merge
# { multi_match: { type: 'most_fields', query: @term, fields: ['text', 'text.stemmed'], operator: 'and' } }
{ match_phrase: { text: { query: @term } } }
end
@ -236,7 +255,7 @@ class SearchQueryTransformer < Parslet::Transform
class PrefixClause
attr_reader :operator, :prefix, :term
def initialize(prefix, operator, term, options = {})
def initialize(prefix, operator, term, options = {}) # rubocop:disable Metrics/CyclomaticComplexity
@prefix = prefix
@negated = operator == '-'
@options = options
@ -274,6 +293,39 @@ class SearchQueryTransformer < Parslet::Transform
when 'in'
@operator = :flag
@term = term
when 'my'
@type = :term
@term = @options[:current_account]&.id
case term
when 'favourited', 'favorited', 'fav'
@filter = :favourited_by
when 'boosted', 'bt'
@filter = :reblogged_by
when 'replied', 'mentioned', 're'
@filter = :mentioned_by
when 'referenced', 'ref'
@filter = :referenced_by
when 'emoji_reacted', 'stamped', 'stamp'
@filter = :emoji_reacted_by
when 'bookmarked', 'bm'
@filter = :bookmarked_by
when 'categoried', 'bmc'
@filter = :bookmark_categoried_by
when 'voted', 'vote'
@filter = :voted_by
when 'interacted', 'act'
@filter = :searchable_by
else
raise "Unknown prefix: my:#{term}"
end
when 'order'
@operator = :order
@term = case term
when 'asc'
term
else
'desc'
end
else
raise "Unknown prefix: #{prefix}"
end
@ -302,7 +354,7 @@ class SearchQueryTransformer < Parslet::Transform
end
def domain_from_term(term)
return '' if %w(local me).include?(term)
return '' if ['local', 'me', Rails.configuration.x.local_domain].include?(term)
term
end

View file

@ -13,6 +13,10 @@
#
class BookmarkCategoryStatus < ApplicationRecord
include Paginable
update_index('statuses', :status) if Chewy.enabled?
belongs_to :bookmark_category
belongs_to :status
belongs_to :bookmark

View file

@ -13,18 +13,48 @@ module StatusSearchConcern
ids << account_id if local?
ids += local_mentioned.pluck(:id)
ids += local_favorited.pluck(:id)
ids += local_reblogged.pluck(:id)
ids += local_bookmarked.pluck(:id)
ids += local_emoji_reacted.pluck(:id)
ids += local_referenced.pluck(:id)
ids += preloadable_poll.local_voters.pluck(:id) if preloadable_poll.present?
ids += mentioned_by
ids += favourited_by
ids += reblogged_by
ids += bookmarked_by
ids += emoji_reacted_by
ids += referenced_by
ids += voted_by if preloadable_poll.present?
ids.uniq
end
end
def mentioned_by
@mentioned_by ||= local_mentioned.pluck(:id)
end
def favourited_by
@favourited_by ||= local_favorited.pluck(:id)
end
def reblogged_by
@reblogged_by ||= local_reblogged.pluck(:id)
end
def bookmarked_by
@bookmarked_by ||= local_bookmarked.pluck(:id)
end
def emoji_reacted_by
@emoji_reacted_by ||= local_emoji_reacted.pluck(:id)
end
def referenced_by
@referenced_by ||= local_referenced.pluck(:id)
end
def voted_by
return [] if preloadable_poll.blank?
@voted_by ||= preloadable_poll.local_voters.pluck(:id)
end
def searchable_text
[
spoiler_text,

View file

@ -86,6 +86,7 @@ class Status < ApplicationRecord
has_many :referenced_by_statuses, through: :referenced_by_status_objects, class_name: 'Status', source: :status
has_many :capability_tokens, class_name: 'StatusCapabilityToken', inverse_of: :status, dependent: :destroy
has_many :bookmark_category_relationships, class_name: 'BookmarkCategoryStatus', inverse_of: :status, dependent: :destroy
has_many :bookmark_categories, class_name: 'BookmarkCategory', through: :bookmark_category_relationships, source: :bookmark_category
has_many :joined_bookmark_categories, class_name: 'BookmarkCategory', through: :bookmark_category_relationships, source: :bookmark_category
# Those associations are used for the private search index
@ -93,6 +94,7 @@ class Status < ApplicationRecord
has_many :local_favorited, -> { merge(Account.local) }, through: :favourites, source: :account
has_many :local_reblogged, -> { merge(Account.local) }, through: :reblogs, source: :account
has_many :local_bookmarked, -> { merge(Account.local) }, through: :bookmarks, source: :account
has_many :local_bookmark_categoried, -> { merge(Account.local) }, through: :bookmark_categories, source: :account
has_many :local_emoji_reacted, -> { merge(Account.local) }, through: :emoji_reactions, source: :account
has_many :local_referenced, -> { merge(Account.local) }, through: :referenced_by_statuses, source: :account
@ -435,6 +437,12 @@ class Status < ApplicationRecord
compute_searchability
end
def searchable_visibility
return limited_scope if limited_visibility? && !none_limited?
visibility
end
class << self
def selectable_visibilities
visibilities.keys - %w(direct limited)

View file

@ -16,8 +16,11 @@ class StatusesSearchService < BaseService
private
def status_search_results
request = parsed_query.request
results = request.collapse(field: :id).order(id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
query = parsed_query
request = query.request
return [] unless query.valid
results = request.collapse(field: :id).order(id: { order: query.order_by }).limit(@limit).offset(@offset).objects.compact
account_ids = results.map(&:account_id)
account_domains = results.map(&:account_domain)
preloaded_relations = @account.relations_map(account_ids, account_domains)