diff --git a/app/lib/search_query_parser.rb b/app/lib/search_query_parser.rb index dfe8b9e9d86c0a..3b1b6c08083689 100644 --- a/app/lib/search_query_parser.rb +++ b/app/lib/search_query_parser.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true class SearchQueryParser < Parslet::Parser - rule(:term) { match('[^\s":]').repeat(1).as(:term) } + 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 } - rule(:shortcode) { (colon >> term >> colon.maybe).as(:shortcode) } + rule(:shortcode) { (operator.maybe >> colon >> term >> colon.maybe).as(:shortcode) } rule(:phrase) { (quote >> (match('[^\s"]').repeat(1).as(:term) >> space.maybe).repeat >> quote).as(:phrase) } rule(:clause) { (operator.maybe >> prefix.maybe.as(:prefix) >> (phrase | term | shortcode)).as(:clause) | prefix.as(:clause) | quote.as(:junk) } rule(:query) { (clause >> space.maybe).repeat.as(:query) } diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index a45ae3d09bc1a7..e3eff4cd27d19b 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -222,10 +222,29 @@ def language_code_from_term(term) end end + class ShortCodeClause + attr_reader :operator, :prefix, :shortcode + + def initialize(operator, shortcode) + @operator = Operator.symbol(operator) + @shortcode = shortcode + end + + def to_query + { multi_match: { type: 'most_fields', query: @shortcode, fields: ['text', 'text.stemmed'], operator: 'and' } } + end + end + rule(clause: subtree(:clause)) do prefix = clause[:prefix][:term].to_s if clause[:prefix] operator = clause[:operator]&.to_s - term = clause[:phrase] ? clause[:phrase].map { |term| term[:term].to_s }.join(' ') : clause[:term].to_s + term = if clause[:phrase] + clause[:phrase].map { |term| term[:term].to_s }.join(' ') + elsif clause[:shortcode] + clause[:shortcode][:term].to_s + else + clause[:term].to_s + end if clause[:prefix] && SUPPORTED_PREFIXES.include?(prefix) PrefixClause.new(prefix, operator, term, current_account: current_account) @@ -235,6 +254,8 @@ def language_code_from_term(term) TermClause.new(operator, term) elsif clause[:phrase] PhraseClause.new(operator, term) + elsif clause[:shortcode] + ShortCodeClause.new(operator, term) else raise "Unexpected clause type: #{clause}" end diff --git a/spec/lib/search_query_parser_spec.rb b/spec/lib/search_query_parser_spec.rb index 66b0e8f9e2367b..2c2e51e8074268 100644 --- a/spec/lib/search_query_parser_spec.rb +++ b/spec/lib/search_query_parser_spec.rb @@ -32,6 +32,10 @@ it 'consumes ":foo:"' do expect(parser.shortcode).to parse(':foo:') end + + it 'consumes "+:foo:"' do + expect(parser.shortcode).to parse('+:foo:') + end end context 'with phrase' do @@ -53,6 +57,10 @@ expect(parser.clause).to parse('foo:bar') end + it 'consumes "+:foo:"' do + expect(parser.clause).to parse('+:foo:') + end + it 'consumes "-foo:bar"' do expect(parser.clause).to parse('-foo:bar') end @@ -94,5 +102,13 @@ it 'consumes "foo:bar bar: hello"' do expect(parser.query).to parse('foo:bar bar: hello') end + + it 'consumes "foo +:bar:"' do + expect(parser.query).to parse('foo +:bar:') + end + + it 'consumes "foo+:bar:"' do + expect(parser.query).to parse('foo+:bar:') + end end end diff --git a/spec/lib/search_query_transformer_spec.rb b/spec/lib/search_query_transformer_spec.rb index 5817e3d1d2015c..35fb22b5e64bd8 100644 --- a/spec/lib/search_query_transformer_spec.rb +++ b/spec/lib/search_query_transformer_spec.rb @@ -58,6 +58,26 @@ end end + context 'with ":foo:"' do + let(:query) { ':foo:' } + + it 'transforms clauses' do + expect(subject.send(:must_clauses).map(&:shortcode)).to contain_exactly('foo') + expect(subject.send(:must_not_clauses)).to be_empty + expect(subject.send(:filter_clauses)).to be_empty + end + end + + context 'with "-:foo:"' do + let(:query) { '-:foo:' } + + it 'transforms clauses' do + expect(subject.send(:must_clauses)).to be_empty + expect(subject.send(:must_not_clauses).map(&:shortcode)).to contain_exactly('foo') + expect(subject.send(:filter_clauses)).to be_empty + end + end + context 'with \'"hello world"\'' do let(:query) { '"hello world"' }