class ScopedSearch::AutoCompleteBuilder

The AutoCompleteBuilder class builds suggestions to complete query based on the query language syntax.

Constants

COMPARISON_OPERATORS
LOGICAL_INFIX_OPERATORS
LOGICAL_PREFIX_OPERATORS
NULL_PREFIX_COMPLETER
NULL_PREFIX_OPERATORS
PREFIX_OPERATORS

Attributes

ast[R]
definition[R]
query[R]
tokens[R]

Public Class Methods

auto_complete(definition, query, options = {}) click to toggle source

This method will parse the query string and build suggestion list using the search query.

   # File lib/scoped_search/auto_complete_builder.rb
18 def self.auto_complete(definition, query, options = {})
19   return [] if (query.nil? or definition.nil? or !definition.respond_to?(:fields))
20 
21   new(definition, query, options).build_autocomplete_options
22 end
new(definition, query, options) click to toggle source

Initializes the instance by setting the relevant parameters

   # File lib/scoped_search/auto_complete_builder.rb
25 def initialize(definition, query, options)
26   @definition = definition
27   @ast        = ScopedSearch::QueryLanguage::Compiler.parse(query)
28   @query      = query
29   @tokens     = tokenize
30   @options    = options
31 end

Public Instance Methods

build_autocomplete_options() click to toggle source

Test the validity of the current query and suggest possible completion

   # File lib/scoped_search/auto_complete_builder.rb
34 def build_autocomplete_options
35   # First parse to find illegal syntax in the existing query,
36   # this method will throw exception on bad syntax.
37   is_query_valid
38 
39   # get the completion options
40   node = last_node
41   completion = complete_options(node)
42 
43   suggestions = []
44   suggestions += complete_keyword        if completion.include?(:keyword)
45   suggestions += LOGICAL_INFIX_OPERATORS if completion.include?(:logical_op)
46   suggestions += LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_COMPLETER if completion.include?(:prefix_op)
47   suggestions += complete_operator(node) if completion.include?(:infix_op)
48   suggestions += complete_value          if completion.include?(:value)
49 
50   build_suggestions(suggestions, completion.include?(:value))
51 end
build_suggestions(suggestions, is_value) click to toggle source
    # File lib/scoped_search/auto_complete_builder.rb
128 def build_suggestions(suggestions, is_value)
129   return [] if (suggestions.blank?)
130 
131   q = query
132   unless q =~ /(\s|\)|,)$/ || last_token_is(COMPARISON_OPERATORS)
133     val = Regexp.escape(tokens.last.to_s).gsub('\*', '.*')
134     suggestions = suggestions.map {|s| s if s.to_s =~ /^"?#{val}"?/i}.compact
135     quoted = /("?#{Regexp.escape(tokens.last.to_s)}"?)$/.match(q)
136     q.chomp!(quoted[1]) if quoted
137   end
138 
139   # for dotted field names compact the suggestions list to be one suggestion
140   # unless the user has typed the relation name entirely or the suggestion list
141   # is short.
142   last_token = tokens.last.to_s
143   if (suggestions.size > 10 && (tokens.empty? || !last_token.include?('.')) && !is_value)
144     suggestions = suggestions.map  do |s|
145       !last_token.empty? && s.to_s.split('.')[0].end_with?(last_token) ? s.to_s : s.to_s.split('.')[0]
146     end
147   end
148 
149   suggestions.uniq.map {|m| "#{q} #{m}"}
150 end
complete_date_value() click to toggle source

date value completer

    # File lib/scoped_search/auto_complete_builder.rb
225 def complete_date_value
226   options = []
227   options << '"30 minutes ago"'
228   options << '"1 hour ago"'
229   options << '"2 hours ago"'
230   options << 'Today'
231   options << 'Yesterday'
232   options << 'Tomorrow'
233   options << 2.days.ago.strftime('%A')
234   options << 3.days.ago.strftime('%A')
235   options << 4.days.ago.strftime('%A')
236   options << 5.days.ago.strftime('%A')
237   options << '"6 days ago"'
238   options << 7.days.ago.strftime('"%b %d,%Y"')
239   options << '2 weeks from now'
240   options
241 end
complete_key(name, field, val) click to toggle source

this method completes the keys list in a key-value schema in the format table.keyName

    # File lib/scoped_search/auto_complete_builder.rb
168 def complete_key(name, field, val)
169   return ["#{name}."] if !val || !val.is_a?(String) || !(val.include?('.'))
170   val = val.sub(/.*\./,'')
171 
172   connection    = definition.klass.connection
173   quoted_table  = field.key_klass.connection.quote_table_name(field.key_klass.table_name)
174   quoted_field  = field.key_klass.connection.quote_column_name(field.key_field)
175   field_name    = "#{quoted_table}.#{quoted_field}"
176 
177   field.key_klass
178     .where(value_conditions(field_name, val))
179     .select(field_name)
180     .limit(20)
181     .distinct
182     .map(&field.key_field)
183     .compact
184     .map { |f| "#{name}.#{f} " }
185 end
complete_key_value(field, token, val) click to toggle source

complete values in a key-value schema

    # File lib/scoped_search/auto_complete_builder.rb
244 def complete_key_value(field, token, val)
245   key_name = token.sub(/^.*\./,"")
246   key_klass = field.key_klass.where(field.key_field => key_name).first
247   raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil?
248 
249   query = completer_scope(field)
250 
251   if field.key_klass != field.klass
252     key   = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym
253     fk    = definition.reflection_by_name(field.klass, key).association_foreign_key.to_sym
254     query = query.where(fk => key_klass.id)
255   end
256 
257   query
258     .where(value_conditions(field.quoted_field, val))
259     .select("DISTINCT #{field.quoted_field}")
260     .limit(20)
261     .map(&field.field)
262     .compact
263     .map { |v| v.to_s =~ /\s/ ? "\"#{v}\"" : v }
264 end
complete_keyword() click to toggle source

suggest all searchable field names. in relations suggest only the long format relation.field.

    # File lib/scoped_search/auto_complete_builder.rb
154 def complete_keyword
155   keywords = []
156   definition.fields.each do|f|
157     next unless f[1].complete_enabled
158     if (f[1].key_field)
159       keywords += complete_key(f[0], f[1], tokens.last)
160     else
161       keywords << f[0].to_s + ' '
162     end
163   end
164   keywords.sort
165 end
complete_operator(node) click to toggle source

This method complete infix operators by field type

    # File lib/scoped_search/auto_complete_builder.rb
272 def complete_operator(node)
273   definition.operator_by_field_name(node.value)
274 end
complete_options(node) click to toggle source

parse the query and return the complete options

   # File lib/scoped_search/auto_complete_builder.rb
54 def complete_options(node)
55 
56   return [:keyword] + [:prefix_op] if tokens.empty?
57 
58   #prefix operator
59   return [:keyword] if last_token_is(PREFIX_OPERATORS)
60 
61   # left hand
62   if is_left_hand(node)
63     if (tokens.size == 1 || last_token_is(PREFIX_OPERATORS + LOGICAL_INFIX_OPERATORS) ||
64         last_token_is(PREFIX_OPERATORS + LOGICAL_INFIX_OPERATORS, 2))
65       options = [:keyword]
66       options += [:prefix_op]  unless last_token_is(PREFIX_OPERATORS)
67     else
68       options = [:logical_op]
69     end
70     return options
71   end
72 
73   if is_right_hand
74     # right hand
75     return [:value]
76   else
77     # comparison operator completer
78     return [:infix_op]
79   end
80 end
complete_set(field) click to toggle source

set value completer

    # File lib/scoped_search/auto_complete_builder.rb
221 def complete_set(field)
222   field.complete_value.keys
223 end
complete_value() click to toggle source

this method auto-completes values of fields that have a :complete_value marker

    # File lib/scoped_search/auto_complete_builder.rb
188 def complete_value
189   if last_token_is(COMPARISON_OPERATORS)
190     token = tokens[tokens.size - 2]
191     val = ''
192   else
193     token = tokens[tokens.size - 3]
194     val = tokens[tokens.size - 1]
195   end
196 
197   field = definition.field_by_name(token)
198   return [] unless field && field.complete_value
199 
200   return complete_set(field) if field.set?
201   return complete_date_value if field.temporal?
202   return complete_key_value(field, token, val) if field.key_field
203 
204   completer_scope(field)
205     .where(value_conditions(field.quoted_field, val))
206     .select(field.quoted_field)
207     .limit(20)
208     .distinct
209     .map(&field.field)
210     .compact
211     .map { |v| v.to_s =~ /\s/ ? "\"#{v.gsub('"', '\"')}\"" : v }
212 end
completer_scope(field) click to toggle source
    # File lib/scoped_search/auto_complete_builder.rb
214 def completer_scope(field)
215   klass = field.klass
216   scope =  klass.respond_to?(:completer_scope) ? klass.completer_scope(@options) : klass
217   scope.respond_to?(:reorder) ? scope.reorder(field.quoted_field) : scope.scoped(:order => field.quoted_field)
218 end
is_left_hand(node) click to toggle source
   # File lib/scoped_search/auto_complete_builder.rb
90 def is_left_hand(node)
91   field = definition.field_by_name(node.value) if node.respond_to?(:value)
92   lh = field.nil? || field.key_field && !(query.end_with?(' '))
93   lh = lh || last_token_is(NULL_PREFIX_OPERATORS, 2)
94   lh = lh && !is_right_hand
95   lh
96 end
is_query_valid() click to toggle source

Test the validity of the existing query, this method will throw exception on illegal query syntax.

   # File lib/scoped_search/auto_complete_builder.rb
84 def is_query_valid
85   # skip test for null prefix operators if in the process of completing the field name.
86   return if(last_token_is(NULL_PREFIX_OPERATORS, 2) && !(query =~ /(\s|\)|,)$/))
87   QueryBuilder.build_query(definition, query)
88 end
is_right_hand() click to toggle source
    # File lib/scoped_search/auto_complete_builder.rb
 98 def is_right_hand
 99   rh = last_token_is(COMPARISON_OPERATORS)
100   if(tokens.size > 1 && !(query.end_with?(' ')))
101     rh = rh || last_token_is(COMPARISON_OPERATORS, 2)
102   end
103   rh
104 end
last_node() click to toggle source
    # File lib/scoped_search/auto_complete_builder.rb
106 def last_node
107   last = ast
108   while (last.kind_of?(ScopedSearch::QueryLanguage::AST::OperatorNode) && !(last.children.empty?)) do
109     last = last.children.last
110   end
111   last
112 end
last_token_is(list,index = 1) click to toggle source
    # File lib/scoped_search/auto_complete_builder.rb
114 def last_token_is(list,index = 1)
115   if tokens.size >= index
116     return list.include?(tokens[tokens.size - index])
117   end
118   return false
119 end
tokenize() click to toggle source
    # File lib/scoped_search/auto_complete_builder.rb
121 def tokenize
122   tokens = ScopedSearch::QueryLanguage::Compiler.tokenize(query)
123   # skip parenthesis, it is not needed for the auto completer.
124   tokens.delete_if {|t| t == :lparen || t == :rparen }
125   tokens
126 end
value_conditions(field_name, val) click to toggle source

This method returns conditions for selecting completion from partial value

    # File lib/scoped_search/auto_complete_builder.rb
267 def value_conditions(field_name, val)
268   val.blank? ? nil : "CAST(#{field_name} as CHAR(50)) LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%')
269 end