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