|
@@ -0,0 +1,86 @@
|
|
|
+# 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
|