gem 'oj', '~> 3.8'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.11'
+gem 'parslet'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.0'
gem 'premailer-rails'
parallel
parser (2.6.3.0)
ast (~> 2.4.0)
+ parslet (1.8.2)
pastel (0.7.2)
equatable (~> 0.5.0)
tty-color (~> 0.4.0)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.29)
+ parslet
pg (~> 1.1)
pghero (~> 2.2)
pkg-config (~> 1.3)
--- /dev/null
+# frozen_string_literal: true
+
+class SearchQueryParser < Parslet::Parser
+ rule(:term) { match('[^\s":]').repeat(1).as(:term) }
+ rule(:quote) { str('"') }
+ rule(:colon) { str(':') }
+ rule(:space) { match('\s').repeat(1) }
+ rule(:operator) { (str('+') | str('-')).as(:operator) }
+ rule(:prefix) { (term >> colon).as(:prefix) }
+ rule(:phrase) { (quote >> (term >> space.maybe).repeat >> quote).as(:phrase) }
+ rule(:clause) { (prefix.maybe >> operator.maybe >> (phrase | term)).as(:clause) }
+ rule(:query) { (clause >> space.maybe).repeat.as(:query) }
+ root(:query)
+end
--- /dev/null
+# frozen_string_literal: true
+
+class SearchQueryTransformer < Parslet::Transform
+ class Query
+ attr_reader :should_clauses, :must_not_clauses, :must_clauses
+
+ def initialize(clauses)
+ grouped = clauses.chunk(&:operator).to_h
+ @should_clauses = grouped.fetch(:should, [])
+ @must_not_clauses = grouped.fetch(:must_not, [])
+ @must_clauses = grouped.fetch(:must, [])
+ end
+
+ def apply(search)
+ should_clauses.each { |clause| search = search.query.should(clause_to_query(clause)) }
+ must_clauses.each { |clause| search = search.query.must(clause_to_query(clause)) }
+ must_not_clauses.each { |clause| search = search.query.must_not(clause_to_query(clause)) }
+ search.query.minimum_should_match(1)
+ end
+
+ private
+
+ def clause_to_query(clause)
+ case clause
+ when TermClause
+ { multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'] } }
+ when PhraseClause
+ { match_phrase: { text: { query: clause.phrase } } }
+ else
+ raise "Unexpected clause type: #{clause}"
+ end
+ end
+ end
+
+ class Operator
+ class << self
+ def symbol(str)
+ case str
+ when '+'
+ :must
+ when '-'
+ :must_not
+ when nil
+ :should
+ else
+ raise "Unknown operator: #{str}"
+ end
+ end
+ end
+ end
+
+ class TermClause
+ attr_reader :prefix, :operator, :term
+
+ def initialize(prefix, operator, term)
+ @prefix = prefix
+ @operator = Operator.symbol(operator)
+ @term = term
+ end
+ end
+
+ class PhraseClause
+ attr_reader :prefix, :operator, :phrase
+
+ def initialize(prefix, operator, phrase)
+ @prefix = prefix
+ @operator = Operator.symbol(operator)
+ @phrase = phrase
+ end
+ end
+
+ rule(clause: subtree(:clause)) do
+ prefix = clause[:prefix][:term].to_s if clause[:prefix]
+ operator = clause[:operator]&.to_s
+
+ if clause[:term]
+ TermClause.new(prefix, operator, clause[:term].to_s)
+ elsif clause[:phrase]
+ PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' '))
+ else
+ raise "Unexpected clause type: #{clause}"
+ end
+ end
+
+ rule(query: sequence(:clauses)) { Query.new(clauses) }
+end
end
def perform_statuses_search!
- definition = StatusesIndex.filter(term: { searchable_by: @account.id })
- .query(multi_match: { type: 'most_fields', query: @query, operator: 'and', fields: %w(text text.stemmed) })
+ definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id }))
if @options[:account_id].present?
definition = definition.filter(term: { account_id: @options[:account_id] })
end
def url_query?
- @options[:type].blank? && @query =~ /\Ahttps?:\/\//
+ @resolve && @options[:type].blank? && @query =~ /\Ahttps?:\/\//
end
def url_resource_results
domain_blocking_by_domain: Account.domain_blocking_map_by_domain(domains, account.id),
}
end
+
+ def parsed_query
+ SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
+ end
end
it 'returns the empty results' do
service = double(call: nil)
allow(ResolveURLService).to receive(:new).and_return(service)
- results = subject.call(@query, nil, 10)
+ results = subject.call(@query, nil, 10, resolve: true)
expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
expect(results).to eq empty_results
service = double(call: account)
allow(ResolveURLService).to receive(:new).and_return(service)
- results = subject.call(@query, nil, 10)
+ results = subject.call(@query, nil, 10, resolve: true)
expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
expect(results).to eq empty_results.merge(accounts: [account])
end
service = double(call: status)
allow(ResolveURLService).to receive(:new).and_return(service)
- results = subject.call(@query, nil, 10)
+ results = subject.call(@query, nil, 10, resolve: true)
expect(service).to have_received(:call).with(@query, on_behalf_of: nil)
expect(results).to eq empty_results.merge(statuses: [status])
end