search_query_transformer.rb 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. # frozen_string_literal: true
  2. class SearchQueryTransformer < Parslet::Transform
  3. SUPPORTED_PREFIXES = %w(
  4. has
  5. is
  6. language
  7. from
  8. before
  9. after
  10. during
  11. in
  12. ).freeze
  13. class Query
  14. def initialize(clauses, options = {})
  15. raise ArgumentError if options[:current_account].nil?
  16. @clauses = clauses
  17. @options = options
  18. flags_from_clauses!
  19. end
  20. def request
  21. search = Chewy::Search::Request.new(*indexes).filter(default_filter)
  22. must_clauses.each { |clause| search = search.query.must(clause.to_query) }
  23. must_not_clauses.each { |clause| search = search.query.must_not(clause.to_query) }
  24. filter_clauses.each { |clause| search = search.filter(**clause.to_query) }
  25. search
  26. end
  27. private
  28. def clauses_by_operator
  29. @clauses_by_operator ||= @clauses.compact.chunk(&:operator).to_h
  30. end
  31. def flags_from_clauses!
  32. @flags = clauses_by_operator.fetch(:flag, []).to_h { |clause| [clause.prefix, clause.term] }
  33. end
  34. def must_clauses
  35. clauses_by_operator.fetch(:must, [])
  36. end
  37. def must_not_clauses
  38. clauses_by_operator.fetch(:must_not, [])
  39. end
  40. def filter_clauses
  41. clauses_by_operator.fetch(:filter, [])
  42. end
  43. def indexes
  44. case @flags['in']
  45. when 'library'
  46. [StatusesIndex]
  47. else
  48. [PublicStatusesIndex, StatusesIndex]
  49. end
  50. end
  51. def default_filter
  52. {
  53. bool: {
  54. should: [
  55. {
  56. term: {
  57. _index: PublicStatusesIndex.index_name,
  58. },
  59. },
  60. {
  61. bool: {
  62. must: [
  63. {
  64. term: {
  65. _index: StatusesIndex.index_name,
  66. },
  67. },
  68. {
  69. term: {
  70. searchable_by: @options[:current_account].id,
  71. },
  72. },
  73. ],
  74. },
  75. },
  76. ],
  77. minimum_should_match: 1,
  78. },
  79. }
  80. end
  81. end
  82. class Operator
  83. class << self
  84. def symbol(str)
  85. case str
  86. when '+', nil
  87. :must
  88. when '-'
  89. :must_not
  90. else
  91. raise "Unknown operator: #{str}"
  92. end
  93. end
  94. end
  95. end
  96. class TermClause
  97. attr_reader :operator, :term
  98. def initialize(operator, term)
  99. @operator = Operator.symbol(operator)
  100. @term = term
  101. end
  102. def to_query
  103. if @term.start_with?('#')
  104. { match: { tags: { query: @term, operator: 'and' } } }
  105. else
  106. { multi_match: { type: 'most_fields', query: @term, fields: ['text', 'text.stemmed'], operator: 'and' } }
  107. end
  108. end
  109. end
  110. class PhraseClause
  111. attr_reader :operator, :phrase
  112. def initialize(operator, phrase)
  113. @operator = Operator.symbol(operator)
  114. @phrase = phrase
  115. end
  116. def to_query
  117. { match_phrase: { text: { query: @phrase } } }
  118. end
  119. end
  120. class PrefixClause
  121. attr_reader :operator, :prefix, :term
  122. def initialize(prefix, operator, term, options = {})
  123. @prefix = prefix
  124. @negated = operator == '-'
  125. @options = options
  126. @operator = :filter
  127. case prefix
  128. when 'has', 'is'
  129. @filter = :properties
  130. @type = :term
  131. @term = term
  132. when 'language'
  133. @filter = :language
  134. @type = :term
  135. @term = language_code_from_term(term)
  136. when 'from'
  137. @filter = :account_id
  138. @type = :term
  139. @term = account_id_from_term(term)
  140. when 'before'
  141. @filter = :created_at
  142. @type = :range
  143. @term = { lt: term, time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
  144. when 'after'
  145. @filter = :created_at
  146. @type = :range
  147. @term = { gt: term, time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
  148. when 'during'
  149. @filter = :created_at
  150. @type = :range
  151. @term = { gte: term, lte: term, time_zone: @options[:current_account]&.user_time_zone.presence || 'UTC' }
  152. when 'in'
  153. @operator = :flag
  154. @term = term
  155. else
  156. raise "Unknown prefix: #{prefix}"
  157. end
  158. end
  159. def to_query
  160. if @negated
  161. { bool: { must_not: { @type => { @filter => @term } } } }
  162. else
  163. { @type => { @filter => @term } }
  164. end
  165. end
  166. private
  167. def account_id_from_term(term)
  168. return @options[:current_account]&.id || -1 if term == 'me'
  169. username, domain = term.gsub(/\A@/, '').split('@')
  170. domain = nil if TagManager.instance.local_domain?(domain)
  171. account = Account.find_remote(username, domain)
  172. # If the account is not found, we want to return empty results, so return
  173. # an ID that does not exist
  174. account&.id || -1
  175. end
  176. def language_code_from_term(term)
  177. language_code = term
  178. return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
  179. language_code = term.downcase
  180. return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
  181. language_code = term.split(/[_-]/).first.downcase
  182. return language_code if LanguagesHelper::SUPPORTED_LOCALES.key?(language_code.to_sym)
  183. term
  184. end
  185. end
  186. rule(clause: subtree(:clause)) do
  187. prefix = clause[:prefix][:term].to_s if clause[:prefix]
  188. operator = clause[:operator]&.to_s
  189. term = clause[:phrase] ? clause[:phrase].map { |term| term[:term].to_s }.join(' ') : clause[:term].to_s
  190. if clause[:prefix] && SUPPORTED_PREFIXES.include?(prefix)
  191. PrefixClause.new(prefix, operator, term, current_account: current_account)
  192. elsif clause[:prefix]
  193. TermClause.new(operator, "#{prefix} #{term}")
  194. elsif clause[:term]
  195. TermClause.new(operator, term)
  196. elsif clause[:phrase]
  197. PhraseClause.new(operator, term)
  198. else
  199. raise "Unexpected clause type: #{clause}"
  200. end
  201. end
  202. rule(junk: subtree(:junk)) do
  203. nil
  204. end
  205. rule(query: sequence(:clauses)) do
  206. Query.new(clauses, current_account: current_account)
  207. end
  208. end