diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index ad3816e4d6..9503fb85a1 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -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 diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb index d152b52039..4bfe3f08ad 100644 --- a/app/chewy/public_statuses_index.rb +++ b/app/chewy/public_statuses_index.rb @@ -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 || '' }) diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 3b83e2f31c..3b0a38a466 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -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 }) diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index 84d3306410..7584dbc819 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -59,13 +59,15 @@ class Search extends PureComponent { defaultOptions = [ { label: <>has: , action: e => { e.preventDefault(); this._insertText('has:') } }, { label: <>is: , action: e => { e.preventDefault(); this._insertText('is:') } }, + { label: <>my: , action: e => { e.preventDefault(); this._insertText('my:') } }, { label: <>language: , action: e => { e.preventDefault(); this._insertText('language:') } }, { label: <>from: , action: e => { e.preventDefault(); this._insertText('from:') } }, { label: <>domain: , action: e => { e.preventDefault(); this._insertText('domain:') } }, { label: <>before: , action: e => { e.preventDefault(); this._insertText('before:') } }, { label: <>during: , action: e => { e.preventDefault(); this._insertText('during:') } }, { label: <>after: , action: e => { e.preventDefault(); this._insertText('after:') } }, - { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:') } } + { label: <>in: , action: e => { e.preventDefault(); this._insertText('in:') } }, + { label: <>order: , action: e => { e.preventDefault(); this._insertText('order:') } }, ]; setRef = c => { diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index 62dd378866..32d30a3b0e 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -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 diff --git a/app/models/bookmark_category_status.rb b/app/models/bookmark_category_status.rb index d932455ff5..8933d0e91f 100644 --- a/app/models/bookmark_category_status.rb +++ b/app/models/bookmark_category_status.rb @@ -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 diff --git a/app/models/concerns/status_search_concern.rb b/app/models/concerns/status_search_concern.rb index 69f1263f81..d1632b7b84 100644 --- a/app/models/concerns/status_search_concern.rb +++ b/app/models/concerns/status_search_concern.rb @@ -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, diff --git a/app/models/status.rb b/app/models/status.rb index d4a28220d1..f9587b3c71 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -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) diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb index de5eda15c6..99921d1957 100644 --- a/app/services/statuses_search_service.rb +++ b/app/services/statuses_search_service.rb @@ -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)