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 212 212: def complete_date_value 213: options =[] 214: options << '"30 minutes ago"' 215: options << '"1 hour ago"' 216: options << '"2 hours ago"' 217: options << 'Today' 218: options << 'Yesterday' 219: options << 2.days.ago.strftime('%A') 220: options << 3.days.ago.strftime('%A') 221: options << 4.days.ago.strftime('%A') 222: options << 5.days.ago.strftime('%A') 223: options << '"6 days ago"' 224: options << 7.days.ago.strftime('"%b %d,%Y"') 225: options 226: 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: table = field.key_klass.connection.quote_table_name(field.key_klass.table_name) 172: field_name = "#{table}.#{field.key_field}" 173: opts = value_conditions(field_name, val).merge(:limit => 20, :select => field_name, :group => field_name ) 174: 175: field.key_klass.all(opts).map(&field.key_field).compact.map{ |f| "#{name}.#{f} "} 176: end
complete values in a key-value schema
# File lib/scoped_search/auto_complete_builder.rb, line 229 229: def complete_key_value(field, token, val) 230: key_name = token.sub(/^.*\./,"") 231: key_opts = value_conditions(field.field,val).merge(:conditions => {field.key_field => key_name}) 232: key_klass = field.key_klass.first(key_opts) 233: raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil? 234: 235: opts = {:select => "DISTINCT #{field.field}"} 236: if(field.key_klass != field.klass) 237: key = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym 238: fk = field.klass.reflections[key].association_foreign_key.to_sym 239: opts.merge!(:conditions => {fk => key_klass.id}) 240: else 241: opts.merge!(key_opts) 242: end 243: return completer_scope(field.klass).all(opts.merge(:limit => 20)).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v} 244: 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 252 252: def complete_operator(node) 253: definition.operator_by_field_name(node.value) 254: 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 208 208: def complete_set(field) 209: field.complete_value.keys 210: end
this method auto-completes values of fields that have a :complete_value marker
# File lib/scoped_search/auto_complete_builder.rb, line 179 179: def complete_value 180: if last_token_is(COMPARISON_OPERATORS) 181: token = tokens[tokens.size-2] 182: val = '' 183: else 184: token = tokens[tokens.size-3] 185: val = tokens[tokens.size-1] 186: end 187: 188: field = definition.field_by_name(token) 189: return [] unless field && field.complete_value 190: 191: return complete_set(field) if field.set? 192: return complete_date_value if field.temporal? 193: return complete_key_value(field, token, val) if field.key_field 194: 195: table = field.klass.connection.quote_table_name(field.klass.table_name) 196: opts = value_conditions("#{table}.#{field.field}", val) 197: opts.merge!(:limit => 20, :select => "DISTINCT #{table}.#{field.field}") 198: 199: return completer_scope(field.klass).all(opts).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v} 200: end
# File lib/scoped_search/auto_complete_builder.rb, line 202 202: def completer_scope(klass) 203: return klass unless klass.respond_to?(:completer_scope) 204: klass.completer_scope(@options) 205: 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