The AutoCompleteBuilder class builds suggestions to complete query based on the query language syntax.
Test the validity of the current query and suggest possible completion
# File lib/scoped_search/auto_complete_builder.rb, line 35 def build_autocomplete_options # First parse to find illegal syntax in the existing query, # this method will throw exception on bad syntax. is_query_valid # get the completion options node = last_node completion = complete_options(node) suggestions = [] suggestions += complete_keyword if completion.include?(:keyword) suggestions += LOGICAL_INFIX_OPERATORS if completion.include?(:logical_op) suggestions += LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_COMPLETER if completion.include?(:prefix_op) suggestions += complete_operator(node) if completion.include?(:infix_op) suggestions += complete_value if completion.include?(:value) build_suggestions(suggestions, completion.include?(:value)) end
# File lib/scoped_search/auto_complete_builder.rb, line 129 def build_suggestions(suggestions, is_value) return [] if (suggestions.blank?) q=query unless q =~ %r(\s|\)|,)$/ || last_token_is(COMPARISON_OPERATORS) val = Regexp.escape(tokens.last.to_s).gsub('\*', '.*') suggestions = suggestions.map {|s| s if s.to_s =~ %r^"?#{val}"?/}.compact quoted = %r("?#{Regexp.escape(tokens.last.to_s)}"?)$/.match(q) q.chomp!(quoted[1]) if quoted end # for doted field names compact the suggestions list to be one suggestion # unless the user has typed the relation name entirely or the suggestion list # is short. if (suggestions.size > 10 && (tokens.empty? || !(tokens.last.to_s.include?('.')) ) && !(is_value)) suggestions = suggestions.map {|s| (s.to_s.split('.')[0].end_with?(tokens.last)) ? s.to_s : s.to_s.split('.')[0] } end suggestions.uniq.map {|m| "#{q} #{m}"} end
date value completer
# File lib/scoped_search/auto_complete_builder.rb, line 215 def complete_date_value options =[] options << '"30 minutes ago"' options << '"1 hour ago"' options << '"2 hours ago"' options << 'Today' options << 'Yesterday' options << 2.days.ago.strftime('%A') options << 3.days.ago.strftime('%A') options << 4.days.ago.strftime('%A') options << 5.days.ago.strftime('%A') options << '"6 days ago"' options << 7.days.ago.strftime('"%b %d,%Y"') options 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 def complete_key(name, field, val) return ["#{name}."] if !val || !val.is_a?(String) || !(val.include?('.')) val = val.sub(%r.*\./,'') connection = definition.klass.connection quoted_table = field.key_klass.connection.quote_table_name(field.key_klass.table_name) quoted_field = field.key_klass.connection.quote_column_name(field.key_field) field_name = "#{quoted_table}.#{quoted_field}" select_clause = "DISTINCT #{field_name}" opts = value_conditions(field_name, val).merge(:select => select_clause, :limit => 20) field.key_klass.all(opts).map(&field.key_field).compact.map{ |f| "#{name}.#{f} "} end
complete values in a key-value schema
# File lib/scoped_search/auto_complete_builder.rb, line 232 def complete_key_value(field, token, val) key_name = token.sub(%r^.*\./,"") key_opts = value_conditions(field.field,val).merge(:conditions => {field.key_field => key_name}) key_klass = field.key_klass.first(key_opts) raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil? opts = {:select => "DISTINCT #{field.field}"} if(field.key_klass != field.klass) key = field.key_klass.to_s.gsub(%r.*::/,'').underscore.to_sym fk = field.klass.reflections[key].association_foreign_key.to_sym opts.merge!(:conditions => {fk => key_klass.id}) else opts.merge!(key_opts) end return completer_scope(field.klass).all(opts.merge(:limit => 20)).map(&field.field).compact.map{|v| v.to_s =~ %r\s+/ ? "\"#{v}\"" : v} 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 def complete_keyword keywords = [] definition.fields.each do|f| if (f[1].key_field) keywords += complete_key(f[0], f[1], tokens.last) else keywords << f[0].to_s+' ' end end keywords.sort end
This method complete infix operators by field type
# File lib/scoped_search/auto_complete_builder.rb, line 255 def complete_operator(node) definition.operator_by_field_name(node.value) end
parse the query and return the complete options
# File lib/scoped_search/auto_complete_builder.rb, line 55 def complete_options(node) return [:keyword] + [:prefix_op] if tokens.empty? #prefix operator return [:keyword] if last_token_is(PREFIX_OPERATORS) # left hand if is_left_hand(node) if (tokens.size == 1 || last_token_is(PREFIX_OPERATORS + LOGICAL_INFIX_OPERATORS) || last_token_is(PREFIX_OPERATORS + LOGICAL_INFIX_OPERATORS, 2)) options = [:keyword] options += [:prefix_op] unless last_token_is(PREFIX_OPERATORS) else options = [:logical_op] end return options end if is_right_hand # right hand return [:value] else # comparison operator completer return [:infix_op] end end
set value completer
# File lib/scoped_search/auto_complete_builder.rb, line 211 def complete_set(field) field.complete_value.keys end
this method auto-completes values of fields that have a :#complete_value marker
# File lib/scoped_search/auto_complete_builder.rb, line 182 def complete_value if last_token_is(COMPARISON_OPERATORS) token = tokens[tokens.size-2] val = '' else token = tokens[tokens.size-3] val = tokens[tokens.size-1] end field = definition.field_by_name(token) return [] unless field && field.complete_value return complete_set(field) if field.set? return complete_date_value if field.temporal? return complete_key_value(field, token, val) if field.key_field table = field.klass.connection.quote_table_name(field.klass.table_name) opts = value_conditions("#{table}.#{field.field}", val) opts.merge!(:limit => 20, :select => "DISTINCT #{table}.#{field.field}") return completer_scope(field.klass).all(opts).map(&field.field).compact.map{|v| v.to_s =~ %r\s+/ ? "\"#{v}\"" : v} end
# File lib/scoped_search/auto_complete_builder.rb, line 205 def completer_scope(klass) return klass unless klass.respond_to?(:completer_scope) klass.completer_scope(@options) end
# File lib/scoped_search/auto_complete_builder.rb, line 91 def is_left_hand(node) field = definition.field_by_name(node.value) if node.respond_to?(:value) lh = field.nil? || field.key_field && !(query.end_with?(' ')) lh = lh || last_token_is(NULL_PREFIX_OPERATORS, 2) lh = lh && !is_right_hand lh 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 def is_query_valid # skip test for null prefix operators if in the process of completing the field name. return if(last_token_is(NULL_PREFIX_OPERATORS, 2) && !(query =~ %r(\s|\)|,)$/)) QueryBuilder.build_query(definition, query) end
# File lib/scoped_search/auto_complete_builder.rb, line 99 def is_right_hand rh = last_token_is(COMPARISON_OPERATORS) if(tokens.size > 1 && !(query.end_with?(' '))) rh = rh || last_token_is(COMPARISON_OPERATORS, 2) end rh end
# File lib/scoped_search/auto_complete_builder.rb, line 107 def last_node last = ast while (last.kind_of?(ScopedSearch::QueryLanguage::AST::OperatorNode) && !(last.children.empty?)) do last = last.children.last end last end
# File lib/scoped_search/auto_complete_builder.rb, line 115 def last_token_is(list,index = 1) if tokens.size >= index return list.include?(tokens[tokens.size - index]) end return false end
# File lib/scoped_search/auto_complete_builder.rb, line 122 def tokenize tokens = ScopedSearch::QueryLanguage::Compiler.tokenize(query) # skip parenthesis, it is not needed for the auto completer. tokens.delete_if {|t| t == :lparen || t == :rparen } tokens end
this method returns conditions for selecting completion from partial value
# File lib/scoped_search/auto_complete_builder.rb, line 250 def value_conditions(field_name, val) return val.blank? ? {} : {:conditions => "#{field_name} LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%')} end
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 def self.auto_complete(definition, query, options = {}) return [] if (query.nil? or definition.nil? or !definition.respond_to?(:fields)) new(definition, query, options).build_autocomplete_options end
Initializes the instance by setting the relevant parameters
# File lib/scoped_search/auto_complete_builder.rb, line 26 def initialize(definition, query, options) @definition = definition @ast = ScopedSearch::QueryLanguage::Compiler.parse(query) @query = query @tokens = tokenize @options = options end