| 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.
| ast | [R] | |
| definition | [R] | |
| query | [R] | |
| tokens | [R] |
This method will parse the query string and build suggestion list using the search query.
# 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
# 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
Test the validity of the current query and suggest possible completion
# 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
# 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
# File lib/scoped_search/auto_complete_builder.rb, line 215
215: def complete_date_value
216: options =[]
217: options << '"30 minutes ago"'
218: options << '"1 hour ago"'
219: options << '"2 hours ago"'
220: options << 'Today'
221: options << 'Yesterday'
222: options << 2.days.ago.strftime('%A')
223: options << 3.days.ago.strftime('%A')
224: options << 4.days.ago.strftime('%A')
225: options << 5.days.ago.strftime('%A')
226: options << '"6 days ago"'
227: options << 7.days.ago.strftime('"%b %d,%Y"')
228: options
229: end
this method completes the keys list in a key-value schema in the format table.keyName
# 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: connection = definition.klass.connection
172: quoted_table = field.key_klass.connection.quote_table_name(field.key_klass.table_name)
173: quoted_field = field.key_klass.connection.quote_column_name(field.key_field)
174: field_name = "#{quoted_table}.#{quoted_field}"
175: select_clause = "DISTINCT #{field_name}"
176: opts = value_conditions(field_name, val).merge(:select => select_clause, :limit => 20)
177:
178: field.key_klass.all(opts).map(&field.key_field).compact.map{ |f| "#{name}.#{f} "}
179: end
complete values in a key-value schema
# File lib/scoped_search/auto_complete_builder.rb, line 232
232: def complete_key_value(field, token, val)
233: key_name = token.sub(/^.*\./,"")
234: key_opts = value_conditions(field.field,val).merge(:conditions => {field.key_field => key_name})
235: key_klass = field.key_klass.first(key_opts)
236: raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil?
237:
238: opts = {:select => "DISTINCT #{field.field}"}
239: if(field.key_klass != field.klass)
240: key = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym
241: fk = field.klass.reflections[key].association_foreign_key.to_sym
242: opts.merge!(:conditions => {fk => key_klass.id})
243: else
244: opts.merge!(key_opts)
245: end
246: return completer_scope(field.klass).all(opts.merge(:limit => 20)).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v}
247: end
suggest all searchable field names. in relations suggest only the long format relation.field.
# 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
# File lib/scoped_search/auto_complete_builder.rb, line 255
255: def complete_operator(node)
256: definition.operator_by_field_name(node.value)
257: end
parse the query and return the complete options
# 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
# File lib/scoped_search/auto_complete_builder.rb, line 211
211: def complete_set(field)
212: field.complete_value.keys
213: end
this method auto-completes values of fields that have a :complete_value marker
# File lib/scoped_search/auto_complete_builder.rb, line 182
182: def complete_value
183: if last_token_is(COMPARISON_OPERATORS)
184: token = tokens[tokens.size-2]
185: val = ''
186: else
187: token = tokens[tokens.size-3]
188: val = tokens[tokens.size-1]
189: end
190:
191: field = definition.field_by_name(token)
192: return [] unless field && field.complete_value
193:
194: return complete_set(field) if field.set?
195: return complete_date_value if field.temporal?
196: return complete_key_value(field, token, val) if field.key_field
197:
198: table = field.klass.connection.quote_table_name(field.klass.table_name)
199: opts = value_conditions("#{table}.#{field.field}", val)
200: opts.merge!(:limit => 20, :select => "DISTINCT #{table}.#{field.field}")
201:
202: return completer_scope(field.klass).all(opts).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v}
203: end
# File lib/scoped_search/auto_complete_builder.rb, line 205
205: def completer_scope(klass)
206: return klass unless klass.respond_to?(:completer_scope)
207: klass.completer_scope(@options)
208: end
# 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.
# 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
# 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
# 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
# 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
# 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