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
Public Class Methods
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
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
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
# 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
date value completer
# File lib/scoped_search/auto_complete_builder.rb 232 def complete_date_value 233 options = [] 234 options << '"30 minutes ago"' 235 options << '"1 hour ago"' 236 options << '"2 hours ago"' 237 options << 'Today' 238 options << 'Yesterday' 239 options << 'Tomorrow' 240 options << 2.days.ago.strftime('%A') 241 options << 3.days.ago.strftime('%A') 242 options << 4.days.ago.strftime('%A') 243 options << 5.days.ago.strftime('%A') 244 options << '"6 days ago"' 245 options << 7.days.ago.strftime('"%b %d,%Y"') 246 options << '"2 weeks from now"' 247 options 248 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 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 values in a key-value schema
# File lib/scoped_search/auto_complete_builder.rb 251 def complete_key_value(field, token, val) 252 key_name = token.sub(/^.*\./,"") 253 key_klass = field.key_klass.where(field.key_field => key_name).first 254 raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil? 255 256 query = completer_scope(field) 257 258 if field.key_klass != field.klass 259 key = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym 260 fk = definition.reflection_by_name(field.klass, key).association_foreign_key.to_sym 261 query = query.where(fk => key_klass.id) 262 end 263 264 query 265 .where(value_conditions(field.quoted_field, val)) 266 .select("DISTINCT #{field.quoted_field}") 267 .limit(20) 268 .map(&field.field) 269 .compact 270 .map { |v| v.to_s =~ /\s/ ? "\"#{v}\"" : v } 271 end
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
This method complete infix operators by field type
# File lib/scoped_search/auto_complete_builder.rb 279 def complete_operator(node) 280 definition.operator_by_field_name(node.value).map { |o| o.end_with?(' ') ? o : "#{o} " } 281 end
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
set value completer
# File lib/scoped_search/auto_complete_builder.rb 228 def complete_set(field) 229 field.complete_value.keys 230 end
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 special_values = field.special_values.select { |v| v =~ /\A#{val}/ } 205 special_values + complete_value_from_db(field, special_values, val) 206 end
# File lib/scoped_search/auto_complete_builder.rb 208 def complete_value_from_db(field, special_values, val) 209 count = 20 - special_values.count 210 completer_scope(field) 211 .where(@options[:value_filter]) 212 .where(value_conditions(field.quoted_field, val)) 213 .select(field.quoted_field) 214 .limit(count) 215 .distinct 216 .map(&field.field) 217 .compact 218 .map { |v| v.to_s =~ /\s/ ? "\"#{v.gsub('"', '\"')}\"" : v } 219 end
# File lib/scoped_search/auto_complete_builder.rb 221 def completer_scope(field) 222 klass = field.klass 223 scope = klass.respond_to?(:completer_scope) ? klass.completer_scope(@options) : klass 224 scope.respond_to?(:reorder) ? scope.reorder(Arel.sql(field.quoted_field)) : scope.scoped(:order => field.quoted_field) 225 end
# 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
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
# 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
# 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
# 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
# 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
This method returns conditions for selecting completion from partial value
# File lib/scoped_search/auto_complete_builder.rb 274 def value_conditions(field_name, val) 275 val.blank? ? nil : "CAST(#{field_name} as CHAR(50)) LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%') 276 end