Module: FullSearch::QueryParser

Defined in:
lib/full_search/query_parser.rb

Defined Under Namespace

Classes: Token

Class Method Summary collapse

Class Method Details

.build_and_expression(nodes) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/full_search/query_parser.rb', line 38

def self.build_and_expression(nodes)
  non_excludes = nodes.reject { |n| n.first == :exclude }
  excludes = nodes.select { |n| n.first == :exclude }

  parts = non_excludes.map { |n| node_to_match(n) }
  result = parts.size == 1 ? parts.first : "(#{parts.join(' AND ')})"

  excludes.each do |node|
    _, value = node
    result = "#{result} NOT \"#{escape(value)}\""
  end

  result
end

.escape(value) ⇒ Object



122
123
124
# File 'lib/full_search/query_parser.rb', line 122

def self.escape(value)
  value.to_s.gsub('"', '""')
end

.node_to_match(node) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/full_search/query_parser.rb', line 53

def self.node_to_match(node)
  type, value = node
  case type
  when :term
    '"' + escape(value) + '"*'
  when :phrase
    '"' + escape(value) + '"'
  when :exclude
    nil
  end
end

.parse(query) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/full_search/query_parser.rb', line 9

def self.parse(query)
  tokens = tokenize(query.to_s.strip)
  return [] if tokens.empty?

  or_clauses = split_or(tokens)

  if or_clauses.size == 1
    parse_and_clause(or_clauses.first)
  else
    [:or, or_clauses.map { |clause| parse_and_clause(clause) }]
  end
end

.parse_and_clause(tokens) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/full_search/query_parser.rb', line 110

def self.parse_and_clause(tokens)
  nodes = tokens.map do |token|
    case token.type
    when :term then [:term, token.value]
    when :phrase then [:phrase, token.value]
    when :exclude then [:exclude, token.value]
    end
  end

  nodes.size == 1 ? nodes.first : [:and, nodes]
end

.split_or(tokens) ⇒ Object



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/full_search/query_parser.rb', line 94

def self.split_or(tokens)
  clauses = []
  current = []

  tokens.each do |token|
    if token.type == :or
      clauses << current unless current.empty?
      current = []
    else
      current << token
    end
  end
  clauses << current unless current.empty?
  clauses
end

.to_match_expression(parsed) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/full_search/query_parser.rb', line 22

def self.to_match_expression(parsed)
  return '""' if parsed.empty?

  type, value = parsed
  case type
  when :and
    build_and_expression(value)
  when :or
    value.map { |sub| to_match_expression(sub) }.join(" OR ")
  else
    node_to_match(parsed)
  end
end

.tokenize(query) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/full_search/query_parser.rb', line 65

def self.tokenize(query)
  tokens = []
  scanner = StringScanner.new(query)

  until scanner.eos?
    scanner.skip(/\s+/)
    break if scanner.eos?

    if scanner.scan(/"/)
      phrase = scanner.scan_until(/"/)
      if phrase
        tokens << Token.new(:phrase, phrase.chomp('"'))
      else
        tokens << Token.new(:term, scanner.rest)
        scanner.terminate
      end
    elsif scanner.scan(/-/)
      scanner.skip(/\s+/)
      tokens << Token.new(:exclude, scanner.scan(/\S+/))
    elsif scanner.scan(/OR|or/i)
      tokens << Token.new(:or, nil)
    else
      tokens << Token.new(:term, scanner.scan(/\S+/))
    end
  end

  tokens
end