Class ScopedSearch::AutoCompleteBuilder
In: lib/scoped_search/auto_complete_builder.rb
Parent: Object

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

Methods

Attributes

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

Public Class methods

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

[Source]

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

Initializes the instance by setting the relevant parameters

[Source]

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

Public Instance methods

Test the validity of the current query and suggest possible completion

[Source]

    # File lib/scoped_search/auto_complete_builder.rb, line 35
35:     def build_autocomplete_options
36:       # First parse to find illegal syntax in the existing query,
37:       # this method will throw exception on bad syntax.
38:       is_query_valid
39: 
40:       # get the completion options
41:       node = last_node
42:       completion = complete_options(node)
43: 
44:       suggestions = []
45:       suggestions += complete_keyword        if completion.include?(:keyword)
46:       suggestions += LOGICAL_INFIX_OPERATORS if completion.include?(:logical_op)
47:       suggestions += LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_COMPLETER if completion.include?(:prefix_op)
48:       suggestions += complete_operator(node) if completion.include?(:infix_op)
49:       suggestions += complete_value          if completion.include?(:value)
50: 
51:       build_suggestions(suggestions, completion.include?(:value))
52:     end

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 129
129:     def build_suggestions(suggestions, is_value)
130:       return [] if (suggestions.blank?)
131: 
132:       q=query
133:       unless q =~ /(\s|\)|,)$/ || last_token_is(COMPARISON_OPERATORS)
134:         val = Regexp.escape(tokens.last.to_s).gsub('\*', '.*')
135:         suggestions = suggestions.map {|s| s if s.to_s =~ /^"?#{val}"?/i}.compact
136:         quoted = /("?#{Regexp.escape(tokens.last.to_s)}"?)$/.match(q)
137:         q.chomp!(quoted[1]) if quoted
138:       end
139: 
140:       # for doted field names compact the suggestions list to be one suggestion
141:       # unless the user has typed the relation name entirely or the suggestion list
142:       # is short.
143:       if (suggestions.size > 10 && (tokens.empty? || !(tokens.last.to_s.include?('.')) ) && !(is_value))
144:         suggestions = suggestions.map {|s|
145:           (s.to_s.split('.')[0].end_with?(tokens.last)) ? s.to_s : s.to_s.split('.')[0]
146:         }
147:       end
148: 
149:       suggestions.uniq.map {|m| "#{q} #{m}"}
150:     end

date value completer

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 212
212:     def complete_date_value
213:       options =[]
214:       options << '"30 minutes ago"'
215:       options << '"1 hour ago"'
216:       options << '"2 hours ago"'
217:       options << 'Today'
218:       options << 'Yesterday'
219:       options << 2.days.ago.strftime('%A')
220:       options << 3.days.ago.strftime('%A')
221:       options << 4.days.ago.strftime('%A')
222:       options << 5.days.ago.strftime('%A')
223:       options << '"6 days ago"'
224:       options << 7.days.ago.strftime('"%b %d,%Y"')
225:       options
226:     end

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

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 167
167:     def complete_key(name, field, val)
168:       return ["#{name}."] if !val || !val.is_a?(String) || !(val.include?('.'))
169:       val = val.sub(/.*\./,'')
170: 
171:       table = field.key_klass.connection.quote_table_name(field.key_klass.table_name)
172:       field_name = "#{table}.#{field.key_field}"
173:       opts =  value_conditions(field_name, val).merge(:limit => 20, :select => field_name, :group => field_name )
174: 
175:       field.key_klass.all(opts).map(&field.key_field).compact.map{ |f| "#{name}.#{f} "}
176:     end

complete values in a key-value schema

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 229
229:     def complete_key_value(field, token, val)
230:       key_name = token.sub(/^.*\./,"")
231:       key_opts = value_conditions(field.field,val).merge(:conditions => {field.key_field => key_name})
232:       key_klass = field.key_klass.first(key_opts)
233:       raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil?
234: 
235:       opts = {:select => "DISTINCT #{field.field}"}
236:       if(field.key_klass != field.klass)
237:         key  = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym
238:         fk   = field.klass.reflections[key].association_foreign_key.to_sym
239:         opts.merge!(:conditions => {fk => key_klass.id})
240:       else
241:         opts.merge!(key_opts)
242:       end
243:       return completer_scope(field.klass).all(opts.merge(:limit => 20)).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v}
244:     end

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

[Source]

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

This method complete infix operators by field type

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 252
252:     def complete_operator(node)
253:       definition.operator_by_field_name(node.value)
254:     end

parse the query and return the complete options

[Source]

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

set value completer

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 208
208:     def complete_set(field)
209:       field.complete_value.keys
210:     end

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

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 179
179:     def complete_value
180:       if last_token_is(COMPARISON_OPERATORS)
181:         token = tokens[tokens.size-2]
182:         val = ''
183:       else
184:         token = tokens[tokens.size-3]
185:         val = tokens[tokens.size-1]
186:       end
187: 
188:       field = definition.field_by_name(token)
189:       return [] unless field && field.complete_value
190: 
191:       return complete_set(field) if field.set?
192:       return complete_date_value if field.temporal?
193:       return complete_key_value(field, token, val) if field.key_field
194: 
195:       table = field.klass.connection.quote_table_name(field.klass.table_name)
196:       opts = value_conditions("#{table}.#{field.field}", val)
197:       opts.merge!(:limit => 20, :select => "DISTINCT #{table}.#{field.field}")
198: 
199:       return completer_scope(field.klass).all(opts).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v}
200:     end

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 202
202:     def completer_scope(klass)
203:       return klass unless klass.respond_to?(:completer_scope)
204:       klass.completer_scope(@options)
205:     end

[Source]

    # File lib/scoped_search/auto_complete_builder.rb, line 91
91:     def is_left_hand(node)
92:       field = definition.field_by_name(node.value) if node.respond_to?(:value)
93:       lh = field.nil? || field.key_field && !(query.end_with?(' '))
94:       lh = lh || last_token_is(NULL_PREFIX_OPERATORS, 2)
95:       lh = lh && !is_right_hand
96:       lh
97:     end

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

[Source]

    # File lib/scoped_search/auto_complete_builder.rb, line 85
85:     def is_query_valid
86:       # skip test for null prefix operators if in the process of completing the field name.
87:       return if(last_token_is(NULL_PREFIX_OPERATORS, 2) && !(query =~ /(\s|\)|,)$/))
88:       QueryBuilder.build_query(definition, query)
89:     end

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 99
 99:     def is_right_hand
100:       rh = last_token_is(COMPARISON_OPERATORS)
101:       if(tokens.size > 1 && !(query.end_with?(' ')))
102:         rh = rh || last_token_is(COMPARISON_OPERATORS, 2)
103:       end
104:       rh
105:     end

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 107
107:     def last_node
108:       last = ast
109:       while (last.kind_of?(ScopedSearch::QueryLanguage::AST::OperatorNode) && !(last.children.empty?)) do
110:         last = last.children.last
111:       end
112:       last
113:     end

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 115
115:     def last_token_is(list,index = 1)
116:       if tokens.size >= index
117:         return list.include?(tokens[tokens.size - index])
118:       end
119:       return false
120:     end

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 122
122:     def tokenize
123:       tokens = ScopedSearch::QueryLanguage::Compiler.tokenize(query)
124:       # skip parenthesis, it is not needed for the auto completer.
125:       tokens.delete_if {|t| t == :lparen || t == :rparen }
126:       tokens
127:     end

this method returns conditions for selecting completion from partial value

[Source]

     # File lib/scoped_search/auto_complete_builder.rb, line 247
247:     def value_conditions(field_name, val)
248:       return val.blank? ? {} : {:conditions => "#{field_name} LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%')}
249:     end

[Validate]