Class: ScopedSearch::AutoCompleteBuilder
- Inherits:
-
Object
- Object
- ScopedSearch::AutoCompleteBuilder
- Defined in:
- lib/scoped_search/auto_complete_builder.rb
Overview
The AutoCompleteBuilder class builds suggestions to complete query based on the query language syntax.
Constant Summary collapse
- LOGICAL_INFIX_OPERATORS =
ScopedSearch::QueryLanguage::Parser::LOGICAL_INFIX_OPERATORS
- LOGICAL_PREFIX_OPERATORS =
ScopedSearch::QueryLanguage::Parser::LOGICAL_PREFIX_OPERATORS
- NULL_PREFIX_OPERATORS =
ScopedSearch::QueryLanguage::Parser::NULL_PREFIX_OPERATORS
- NULL_PREFIX_COMPLETER =
['has']
- COMPARISON_OPERATORS =
ScopedSearch::QueryLanguage::Parser::COMPARISON_OPERATORS
- PREFIX_OPERATORS =
LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_OPERATORS
Instance Attribute Summary collapse
-
#ast ⇒ Object
readonly
Returns the value of attribute ast.
-
#definition ⇒ Object
readonly
Returns the value of attribute definition.
-
#query ⇒ Object
readonly
Returns the value of attribute query.
-
#tokens ⇒ Object
readonly
Returns the value of attribute tokens.
Class Method Summary collapse
-
.auto_complete(definition, query, options = {}) ⇒ Object
This method will parse the query string and build suggestion list using the search query.
Instance Method Summary collapse
-
#build_autocomplete_options ⇒ Object
Test the validity of the current query and suggest possible completion.
- #build_suggestions(suggestions, is_value) ⇒ Object
-
#complete_date_value ⇒ Object
date value completer.
-
#complete_key(name, field, val) ⇒ Object
this method completes the keys list in a key-value schema in the format table.keyName.
-
#complete_key_value(field, token, val) ⇒ Object
complete values in a key-value schema.
-
#complete_keyword ⇒ Object
suggest all searchable field names.
-
#complete_operator(node) ⇒ Object
This method complete infix operators by field type.
-
#complete_options(node) ⇒ Object
parse the query and return the complete options.
-
#complete_set(field) ⇒ Object
set value completer.
-
#complete_value ⇒ Object
this method auto-completes values of fields that have a :complete_value marker.
- #complete_value_from_db(field, special_values, val) ⇒ Object
- #completer_scope(field) ⇒ Object
-
#initialize(definition, query, options) ⇒ AutoCompleteBuilder
constructor
Initializes the instance by setting the relevant parameters.
- #is_left_hand(node) ⇒ Object
-
#is_query_valid ⇒ Object
Test the validity of the existing query, this method will throw exception on illegal query syntax.
- #is_right_hand ⇒ Object
- #last_node ⇒ Object
- #last_token_is(list, index = 1) ⇒ Object
- #tokenize ⇒ Object
-
#value_conditions(field_name, val) ⇒ Object
This method returns conditions for selecting completion from partial value.
Constructor Details
#initialize(definition, query, options) ⇒ AutoCompleteBuilder
Initializes the instance by setting the relevant parameters
25 26 27 28 29 30 31 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 25 def initialize(definition, query, ) @definition = definition @ast = ScopedSearch::QueryLanguage::Compiler.parse(query) @query = query @tokens = tokenize @options = end |
Instance Attribute Details
#ast ⇒ Object (readonly)
Returns the value of attribute ast.
14 15 16 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 14 def ast @ast end |
#definition ⇒ Object (readonly)
Returns the value of attribute definition.
14 15 16 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 14 def definition @definition end |
#query ⇒ Object (readonly)
Returns the value of attribute query.
14 15 16 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 14 def query @query end |
#tokens ⇒ Object (readonly)
Returns the value of attribute tokens.
14 15 16 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 14 def tokens @tokens end |
Class Method Details
.auto_complete(definition, query, options = {}) ⇒ Object
This method will parse the query string and build suggestion list using the search query.
18 19 20 21 22 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 18 def self.auto_complete(definition, query, = {}) return [] if (query.nil? or definition.nil? or !definition.respond_to?(:fields)) new(definition, query, ). end |
Instance Method Details
#build_autocomplete_options ⇒ Object
Test the validity of the current query and suggest possible completion
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 34 def # 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 = (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) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 128 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}"?/i}.compact quoted = /("?#{Regexp.escape(tokens.last.to_s)}"?)$/.match(q) q.chomp!(quoted[1]) if quoted end # for dotted 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. last_token = tokens.last.to_s if (suggestions.size > 10 && (tokens.empty? || !last_token.include?('.')) && !is_value) suggestions = suggestions.map do |s| !last_token.empty? && s.to_s.split('.')[0].end_with?(last_token) ? s.to_s : s.to_s.split('.')[0] end end suggestions.uniq.map {|m| "#{q} #{m}"} end |
#complete_date_value ⇒ Object
date value completer
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 241 def complete_date_value = [] << '"30 minutes ago"' << '"1 hour ago"' << '"2 hours ago"' << 'Today' << 'Yesterday' << 'Tomorrow' << 2.days.ago.strftime('%A') << 3.days.ago.strftime('%A') << 4.days.ago.strftime('%A') << 5.days.ago.strftime('%A') << '"6 days ago"' << 7.days.ago.strftime('"%b %d,%Y"') << '"2 weeks from now"' end |
#complete_key(name, field, val) ⇒ Object
this method completes the keys list in a key-value schema in the format table.keyName
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 168 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}" field.key_klass .where(value_conditions(field_name, val)) .select(field_name) .limit(20) .distinct .map(&field.key_field) .compact .map { |f| "#{name}.#{f} " } end |
#complete_key_value(field, token, val) ⇒ Object
complete values in a key-value schema
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 260 def complete_key_value(field, token, val) key_name = token.sub(/^.*\./,"") key_klass = field.key_klass.where(field.key_field => key_name).first raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil? query = completer_scope(field) if field.key_klass != field.klass key = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym fk = definition.reflection_by_name(field.klass, key).association_foreign_key.to_sym query = query.where(fk => key_klass.id) end query .where(value_conditions(field.quoted_field, val)) .select("DISTINCT #{field.quoted_field}") .limit(20) .map(&field.field) .compact .map { |v| v.to_s =~ /\s/ ? "\"#{v}\"" : v } end |
#complete_keyword ⇒ Object
suggest all searchable field names. in relations suggest only the long format relation.field.
154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 154 def complete_keyword keywords = [] definition.fields.each do|f| next unless f[1].complete_enabled 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) ⇒ Object
This method complete infix operators by field type
288 289 290 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 288 def complete_operator(node) definition.operator_by_field_name(node.value).map { |o| o.end_with?(' ') ? o : "#{o} " } end |
#complete_options(node) ⇒ Object
parse the query and return the complete options
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 54 def (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)) = [:keyword] += [:prefix_op] unless last_token_is(PREFIX_OPERATORS) else = [:logical_op] end return end if is_right_hand # right hand return [:value] else # comparison operator completer return [:infix_op] end end |
#complete_set(field) ⇒ Object
set value completer
237 238 239 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 237 def complete_set(field) field.complete_value.keys end |
#complete_value ⇒ Object
this method auto-completes values of fields that have a :complete_value marker
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 188 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 special_values = field.special_values.select { |v| v =~ /\A#{val}/ } special_values + complete_value_from_db(field, special_values, val) end |
#complete_value_from_db(field, special_values, val) ⇒ Object
208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 208 def complete_value_from_db(field, special_values, val) count = 20 - special_values.count completer_scope(field) .where(@options[:value_filter]) .where(value_conditions(field.quoted_field, val)) .select(field.quoted_field) .limit(count) .distinct .map(&field.field) .compact .map { |v| v.is_a?(String) ? "\"#{v.gsub('"', '\"')}\"" : v } end |
#completer_scope(field) ⇒ Object
222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 222 def completer_scope(field) scope = field.klass scope = scope.completer_scope(@options) if scope.respond_to?(:completer_scope) if field.klass != field.definition.klass reflection = field.definition.reflection_by_name(field.definition.klass, field.relation) sub_scope = reflection.active_record.joins(reflection.name) sub_scope = sub_scope.select("#{field.klass.table_name}.#{field.klass.primary_key}") scope = scope.where(field.klass.primary_key => sub_scope) end scope.respond_to?(:reorder) ? scope.reorder(Arel.sql(field.quoted_field)) : scope.scoped(:order => field.quoted_field) end |
#is_left_hand(node) ⇒ Object
90 91 92 93 94 95 96 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 90 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 ⇒ Object
Test the validity of the existing query, this method will throw exception on illegal query syntax.
84 85 86 87 88 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 84 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 ⇒ Object
98 99 100 101 102 103 104 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 98 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 ⇒ Object
106 107 108 109 110 111 112 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 106 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) ⇒ Object
114 115 116 117 118 119 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 114 def last_token_is(list,index = 1) if tokens.size >= index return list.include?(tokens[tokens.size - index]) end return false end |
#tokenize ⇒ Object
121 122 123 124 125 126 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 121 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) ⇒ Object
This method returns conditions for selecting completion from partial value
283 284 285 |
# File 'lib/scoped_search/auto_complete_builder.rb', line 283 def value_conditions(field_name, val) val.blank? ? nil : "CAST(#{field_name} as CHAR(50)) LIKE '#{val.gsub("'","''")}%'".tr_s('%*', '%') end |