Parent

Files

ScopedSearch::AutoCompleteBuilder

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

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, 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
new(definition, query, options) click to toggle source

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

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, 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
build_suggestions(suggestions, is_value) click to toggle source
# File lib/scoped_search/auto_complete_builder.rb, line 129
def build_suggestions(suggestions, is_value)
  return [] if (suggestions.blank?)

  q=query
  unless q =~ /(\s|\)|,)$/ || last_token_is(COMPARISON_OPERATORS)
    val = Regexp.escape(tokens.last.to_s).gsub('\*', '.*')
    suggestions = suggestions.map {|s| s if s.to_s =~ /^"?#{val}"?/}.compact
    quoted = /("?#{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
complete_date_value() click to toggle source

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
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, line 167
def complete_key(name, field, val)
  return ["#{name}."] if !val || !val.is_a?(String) || !(val.include?('.'))
  val = val.sub(/.*\./,'')

  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_key_value(field, token, val) click to toggle source

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(/^.*\./,"")
  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(/.*::/,'').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 =~ /\s+/ ? "\"#{v}\"" : v}
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, 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
complete_operator(node) click to toggle source

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
complete_options(node) click to toggle source

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
complete_set(field) click to toggle source

set value completer

# File lib/scoped_search/auto_complete_builder.rb, line 211
def complete_set(field)
  field.complete_value.keys
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, 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 =~ /\s+/ ? "\"#{v}\"" : v}
end
completer_scope(klass) click to toggle source
# 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
is_left_hand(node) click to toggle source
# 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
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, 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 =~ /(\s|\)|,)$/))
  QueryBuilder.build_query(definition, query)
end
is_right_hand() click to toggle source
# 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
last_node() click to toggle source
# 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
last_token_is(list,index = 1) click to toggle source
# 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
tokenize() click to toggle source
# 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
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, line 250
def value_conditions(field_name, val)
  return val.blank? ? {} : {:conditions => "#{field_name} LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%')}
end

[Validate]

Generated with the Darkfish Rdoc Generator 2.