scoped_search 2.2.1 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/.infinity_test +8 -0
- data/Gemfile +15 -0
- data/lib/scoped_search/auto_complete_builder.rb +251 -0
- data/lib/scoped_search/definition.rb +90 -10
- data/lib/scoped_search/query_builder.rb +209 -45
- data/lib/scoped_search/query_language/parser.rb +4 -2
- data/lib/scoped_search/query_language/tokenizer.rb +3 -3
- data/lib/scoped_search/rails_helper.rb +210 -0
- data/lib/scoped_search.rb +9 -1
- data/scoped_search.gemspec +7 -5
- data/spec/database.yml +13 -6
- data/spec/integration/api_spec.rb +1 -1
- data/spec/integration/auto_complete_spec.rb +140 -0
- data/spec/integration/key_value_querying_spec.rb +87 -0
- data/spec/integration/ordinal_querying_spec.rb +105 -10
- data/spec/integration/profile_querying_spec.rb +1 -1
- data/spec/integration/relation_querying_spec.rb +186 -194
- data/spec/integration/set_query_spec.rb +76 -0
- data/spec/integration/string_querying_spec.rb +19 -2
- data/spec/lib/matchers.rb +10 -0
- data/spec/spec_helper.rb +2 -4
- data/spec/unit/ast_spec.rb +1 -1
- data/spec/unit/auto_complete_builder_spec.rb +20 -0
- data/spec/unit/definition_spec.rb +1 -1
- data/spec/unit/parser_spec.rb +1 -1
- data/spec/unit/query_builder_spec.rb +2 -1
- data/spec/unit/tokenizer_spec.rb +1 -1
- data/tasks/github-gem.rake +18 -14
- metadata +33 -7
data/.gitignore
CHANGED
data/.infinity_test
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source :rubygems
|
2
|
+
gemspec
|
3
|
+
|
4
|
+
# ScopedSearch is always tested on a sqlite3 memory database.
|
5
|
+
# Uncomment the following gems to test scoped search on other RDBMSs.
|
6
|
+
# You can specify the databasde connection details in spec/database.yml
|
7
|
+
|
8
|
+
gem 'mysql2', '~> 0.2.6' # preferred over 'mysql'
|
9
|
+
#gem 'mysql'
|
10
|
+
# gem 'postgresql'
|
11
|
+
|
12
|
+
# # JDBC connections
|
13
|
+
# gem 'activerecord-jdbcsqlite3-adapter'
|
14
|
+
# gem 'activerecord-jdbcmysql-adapter'
|
15
|
+
# gem 'activerecord-jdbcpostgresql-adapter'
|
@@ -0,0 +1,251 @@
|
|
1
|
+
module ScopedSearch
|
2
|
+
|
3
|
+
|
4
|
+
LOGICAL_INFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::LOGICAL_INFIX_OPERATORS
|
5
|
+
LOGICAL_PREFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::LOGICAL_PREFIX_OPERATORS
|
6
|
+
NULL_PREFIX_OPERATORS = ScopedSearch::QueryLanguage::Parser::NULL_PREFIX_OPERATORS
|
7
|
+
NULL_PREFIX_COMPLETER = ['has']
|
8
|
+
COMPARISON_OPERATORS = ScopedSearch::QueryLanguage::Parser::COMPARISON_OPERATORS
|
9
|
+
PREFIX_OPERATORS = LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_OPERATORS
|
10
|
+
|
11
|
+
# The AutoCompleteBuilder class builds suggestions to complete query based on
|
12
|
+
# the query language syntax.
|
13
|
+
class AutoCompleteBuilder
|
14
|
+
|
15
|
+
attr_reader :ast, :definition, :query, :tokens
|
16
|
+
|
17
|
+
# This method will parse the query string and build suggestion list using the
|
18
|
+
# search query.
|
19
|
+
def self.auto_complete(definition, query)
|
20
|
+
return [] if (query.nil? or definition.nil? or !definition.respond_to?(:fields))
|
21
|
+
|
22
|
+
new(definition, query).build_autocomplete_options
|
23
|
+
end
|
24
|
+
|
25
|
+
# Initializes the instance by setting the relevant parameters
|
26
|
+
def initialize(definition, query)
|
27
|
+
@definition = definition
|
28
|
+
@ast = ScopedSearch::QueryLanguage::Compiler.parse(query)
|
29
|
+
@query = query
|
30
|
+
@tokens = tokenize
|
31
|
+
end
|
32
|
+
|
33
|
+
# Test the validity of the current query and suggest possible completion
|
34
|
+
def build_autocomplete_options
|
35
|
+
# First parse to find illegal syntax in the existing query,
|
36
|
+
# this method will throw exception on bad syntax.
|
37
|
+
is_query_valid
|
38
|
+
|
39
|
+
# get the completion options
|
40
|
+
node = last_node
|
41
|
+
completion = complete_options(node)
|
42
|
+
|
43
|
+
suggestions = []
|
44
|
+
suggestions += complete_keyword if completion.include?(:keyword)
|
45
|
+
suggestions += LOGICAL_INFIX_OPERATORS if completion.include?(:logical_op)
|
46
|
+
suggestions += LOGICAL_PREFIX_OPERATORS + NULL_PREFIX_COMPLETER if completion.include?(:prefix_op)
|
47
|
+
suggestions += complete_operator(node) if completion.include?(:infix_op)
|
48
|
+
suggestions += complete_value if completion.include?(:value)
|
49
|
+
|
50
|
+
build_suggestions(suggestions, completion.include?(:value))
|
51
|
+
end
|
52
|
+
|
53
|
+
# parse the query and return the complete options
|
54
|
+
def complete_options(node)
|
55
|
+
|
56
|
+
return [:keyword] + [:prefix_op] if tokens.empty?
|
57
|
+
|
58
|
+
#prefix operator
|
59
|
+
return [:keyword] if last_token_is(PREFIX_OPERATORS)
|
60
|
+
|
61
|
+
# left hand
|
62
|
+
if is_left_hand(node)
|
63
|
+
if (tokens.size == 1 || last_token_is(PREFIX_OPERATORS + LOGICAL_INFIX_OPERATORS) ||
|
64
|
+
last_token_is(PREFIX_OPERATORS + LOGICAL_INFIX_OPERATORS, 2))
|
65
|
+
options = [:keyword]
|
66
|
+
options += [:prefix_op] unless last_token_is(PREFIX_OPERATORS)
|
67
|
+
else
|
68
|
+
options = [:logical_op]
|
69
|
+
end
|
70
|
+
return options
|
71
|
+
end
|
72
|
+
|
73
|
+
if is_right_hand
|
74
|
+
# right hand
|
75
|
+
return [:value]
|
76
|
+
else
|
77
|
+
# comparison operator completer
|
78
|
+
return [:infix_op]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Test the validity of the existing query, this method will throw exception on illegal
|
83
|
+
# query syntax.
|
84
|
+
def is_query_valid
|
85
|
+
# skip test for null prefix operators if in the process of completing the field name.
|
86
|
+
return if(last_token_is(NULL_PREFIX_OPERATORS, 2) && !(query =~ /(\s|\)|,)$/))
|
87
|
+
QueryBuilder.build_query(definition, query)
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_left_hand(node)
|
91
|
+
field = definition.field_by_name(node.value)
|
92
|
+
lh = field.nil? || field.key_field && !(query.end_with?(' '))
|
93
|
+
lh = lh || last_token_is(NULL_PREFIX_OPERATORS, 2)
|
94
|
+
lh = lh && !is_right_hand
|
95
|
+
lh
|
96
|
+
end
|
97
|
+
|
98
|
+
def is_right_hand
|
99
|
+
rh = last_token_is(COMPARISON_OPERATORS)
|
100
|
+
if(tokens.size > 1 && !(query.end_with?(' ')))
|
101
|
+
rh = rh || last_token_is(COMPARISON_OPERATORS, 2)
|
102
|
+
end
|
103
|
+
rh
|
104
|
+
end
|
105
|
+
|
106
|
+
def last_node
|
107
|
+
last = ast
|
108
|
+
while (last.kind_of?(ScopedSearch::QueryLanguage::AST::OperatorNode) && !(last.children.empty?)) do
|
109
|
+
last = last.children.last
|
110
|
+
end
|
111
|
+
last
|
112
|
+
end
|
113
|
+
|
114
|
+
def last_token_is(list,index = 1)
|
115
|
+
if tokens.size >= index
|
116
|
+
return list.include?(tokens[tokens.size - index])
|
117
|
+
end
|
118
|
+
return false
|
119
|
+
end
|
120
|
+
|
121
|
+
def tokenize
|
122
|
+
tokens = ScopedSearch::QueryLanguage::Compiler.tokenize(query)
|
123
|
+
# skip parenthesis, it is not needed for the auto completer.
|
124
|
+
tokens.delete_if {|t| t == :lparen || t == :rparen }
|
125
|
+
tokens
|
126
|
+
end
|
127
|
+
|
128
|
+
def build_suggestions(suggestions, is_value)
|
129
|
+
return [] if (suggestions.blank?)
|
130
|
+
|
131
|
+
q=query
|
132
|
+
unless q =~ /(\s|\)|,)$/
|
133
|
+
val = Regexp.escape(tokens.last.to_s).gsub('\*', '.*')
|
134
|
+
suggestions = suggestions.map {|s| s if s.to_s =~ /^#{val}/i}.compact
|
135
|
+
q.chomp!(tokens.last.to_s)
|
136
|
+
end
|
137
|
+
|
138
|
+
# for doted field names compact the suggestions list to be one suggestion
|
139
|
+
# unless the user has typed the relation name entirely or the suggestion list
|
140
|
+
# is short.
|
141
|
+
if (suggestions.size > 10 && (tokens.empty? || !(tokens.last.to_s.include?('.')) ) && !(is_value))
|
142
|
+
suggestions = suggestions.map {|s|
|
143
|
+
(s.to_s.split('.')[0].end_with?(tokens.last)) ? s.to_s : s.to_s.split('.')[0]
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
suggestions.uniq.map {|m| "#{q} #{m}".gsub(/\s+/," ")}
|
148
|
+
end
|
149
|
+
|
150
|
+
# suggest all searchable field names.
|
151
|
+
# in relations suggest only the long format relation.field.
|
152
|
+
def complete_keyword
|
153
|
+
keywords = []
|
154
|
+
definition.fields.each do|f|
|
155
|
+
if (f[1].key_field)
|
156
|
+
keywords += complete_key(f[0], f[1], tokens.last)
|
157
|
+
else
|
158
|
+
keywords << f[0].to_s+' '
|
159
|
+
end
|
160
|
+
end
|
161
|
+
keywords.sort
|
162
|
+
end
|
163
|
+
|
164
|
+
#this method completes the keys list in a key-value schema in the format table.keyName
|
165
|
+
def complete_key(name, field, val)
|
166
|
+
return ["#{name}."] if !val || !val.is_a?(String) || !(val.include?('.'))
|
167
|
+
val = val.sub(/.*\./,'')
|
168
|
+
|
169
|
+
field_name = field.key_field
|
170
|
+
opts = value_conditions(field.key_field, val).merge(:limit => 20, :select => field_name, :group => field_name )
|
171
|
+
|
172
|
+
field.key_klass.all(opts).map(&field_name).compact.map{ |f| "#{name}.#{f} "}
|
173
|
+
end
|
174
|
+
|
175
|
+
# this method auto-completes values of fields that have a :complete_value marker
|
176
|
+
def complete_value
|
177
|
+
if last_token_is(COMPARISON_OPERATORS)
|
178
|
+
token = tokens[tokens.size-2]
|
179
|
+
val = ''
|
180
|
+
else
|
181
|
+
token = tokens[tokens.size-3]
|
182
|
+
val = tokens[tokens.size-1]
|
183
|
+
end
|
184
|
+
|
185
|
+
field = definition.field_by_name(token)
|
186
|
+
return [] unless field && field.complete_value
|
187
|
+
|
188
|
+
return complete_set(field) if field.set?
|
189
|
+
return complete_date_value if field.temporal?
|
190
|
+
return complete_key_value(field, token, val) if field.key_field
|
191
|
+
|
192
|
+
opts = value_conditions(field.field, val)
|
193
|
+
opts.merge!(:limit => 20, :select => "DISTINCT #{field.field}")
|
194
|
+
return field.klass.all(opts).map(&field.field).compact.map{|v| v.to_s =~ /\s+/ ? "\"#{v}\"" : v}
|
195
|
+
end
|
196
|
+
|
197
|
+
# set value completer
|
198
|
+
def complete_set(field)
|
199
|
+
field.complete_value.keys
|
200
|
+
end
|
201
|
+
# date value completer
|
202
|
+
def complete_date_value
|
203
|
+
options =[]
|
204
|
+
options << '"30 minutes ago"'
|
205
|
+
options << '"1 hour ago"'
|
206
|
+
options << '"2 hours ago"'
|
207
|
+
options << 'Today'
|
208
|
+
options << 'Yesterday'
|
209
|
+
options << 2.days.ago.strftime('%A')
|
210
|
+
options << 3.days.ago.strftime('%A')
|
211
|
+
options << 4.days.ago.strftime('%A')
|
212
|
+
options << 5.days.ago.strftime('%A')
|
213
|
+
options << '"6 days ago"'
|
214
|
+
options << 7.days.ago.strftime('"%b %d,%Y"')
|
215
|
+
options
|
216
|
+
end
|
217
|
+
|
218
|
+
# complete values in a key-value schema
|
219
|
+
def complete_key_value(field, token, val)
|
220
|
+
key_name = token.sub(/^.*\./,"")
|
221
|
+
key_opts = value_conditions(field.field,val).merge(:conditions => {field.key_field => key_name})
|
222
|
+
key_klass = field.key_klass.first(key_opts)
|
223
|
+
raise ScopedSearch::QueryNotSupported, "Field '#{key_name}' not recognized for searching!" if key_klass.nil?
|
224
|
+
|
225
|
+
opts = {:select => "DISTINCT #{field.field}"}
|
226
|
+
if(field.key_klass != field.klass)
|
227
|
+
key = field.key_klass.to_s.gsub(/.*::/,'').underscore.to_sym
|
228
|
+
fk = field.klass.reflections[key].association_foreign_key.to_sym
|
229
|
+
opts.merge!(:conditions => {fk => key_klass.id})
|
230
|
+
else
|
231
|
+
opts.merge!(key_opts)
|
232
|
+
end
|
233
|
+
return field.klass.all(opts.merge(:limit => 20)).map(&field.field)
|
234
|
+
end
|
235
|
+
|
236
|
+
#this method returns conditions for selecting completion from partial value
|
237
|
+
def value_conditions(field_name, val)
|
238
|
+
return val.blank? ? {} : {:conditions => "#{field_name} LIKE '#{val}%'".tr_s('%*', '%')}
|
239
|
+
end
|
240
|
+
|
241
|
+
# This method complete infix operators by field type
|
242
|
+
def complete_operator(node)
|
243
|
+
definition.operator_by_field_name(node.value)
|
244
|
+
end
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
# Load lib files
|
251
|
+
require 'scoped_search/query_builder'
|
@@ -15,11 +15,24 @@ module ScopedSearch
|
|
15
15
|
# class, so you should not create instances of this class yourself.
|
16
16
|
class Field
|
17
17
|
|
18
|
-
attr_reader :definition, :field, :only_explicit, :relation
|
18
|
+
attr_reader :definition, :field, :only_explicit, :relation, :key_relation,
|
19
|
+
:key_field, :complete_value, :offset, :word_size, :ext_method, :operators
|
19
20
|
|
20
21
|
# The ActiveRecord-based class that belongs to this field.
|
21
22
|
def klass
|
22
23
|
if relation
|
24
|
+
related = definition.klass.reflections[relation]
|
25
|
+
raise ScopedSearch::QueryNotSupported, "relation '#{relation}' not one of #{definition.klass.reflections.keys.join(', ')} " if related.nil?
|
26
|
+
related.klass
|
27
|
+
else
|
28
|
+
definition.klass
|
29
|
+
end
|
30
|
+
end
|
31
|
+
# The ActiveRecord-based class that belongs the key field in a key-value pair.
|
32
|
+
def key_klass
|
33
|
+
if key_relation
|
34
|
+
definition.klass.reflections[key_relation].klass
|
35
|
+
elsif relation
|
23
36
|
definition.klass.reflections[relation].klass
|
24
37
|
else
|
25
38
|
definition.klass
|
@@ -62,6 +75,11 @@ module ScopedSearch
|
|
62
75
|
[:string, :text].include?(type)
|
63
76
|
end
|
64
77
|
|
78
|
+
# Returns true if this is a set.
|
79
|
+
def set?
|
80
|
+
complete_value.is_a?(Hash)
|
81
|
+
end
|
82
|
+
|
65
83
|
# Returns the default search operator for this field.
|
66
84
|
def default_operator
|
67
85
|
@default_operator ||= case type
|
@@ -70,12 +88,20 @@ module ScopedSearch
|
|
70
88
|
end
|
71
89
|
end
|
72
90
|
|
91
|
+
def default_order(options)
|
92
|
+
return nil if options[:default_order].nil?
|
93
|
+
field_name = options[:on] unless options[:rename]
|
94
|
+
field_name = options[:rename] if options[:rename]
|
95
|
+
order = (options[:default_order].to_s.downcase.include?('desc')) ? "DESC" : "ASC"
|
96
|
+
return "#{field_name} #{order}"
|
97
|
+
end
|
73
98
|
# Initializes a Field instance given the definition passed to the
|
74
99
|
# scoped_search call on the ActiveRecord-based model class.
|
75
100
|
def initialize(definition, options = {})
|
76
101
|
@definition = definition
|
77
102
|
@definition.profile = options[:profile] if options[:profile]
|
78
|
-
|
103
|
+
@definition.default_order ||= default_order(options)
|
104
|
+
|
79
105
|
case options
|
80
106
|
when Symbol, String
|
81
107
|
@field = field.to_sym
|
@@ -83,18 +109,26 @@ module ScopedSearch
|
|
83
109
|
@field = options.delete(:on)
|
84
110
|
|
85
111
|
# Set attributes from options hash
|
112
|
+
@complete_value = options[:complete_value]
|
86
113
|
@relation = options[:in]
|
114
|
+
@key_relation = options[:in_key]
|
115
|
+
@key_field = options[:on_key]
|
116
|
+
@offset = options[:offset]
|
117
|
+
@word_size = options[:word_size] || 1
|
118
|
+
@ext_method = options[:ext_method]
|
119
|
+
@operators = options[:operators]
|
87
120
|
@only_explicit = !!options[:only_explicit]
|
88
121
|
@default_operator = options[:default_operator] if options.has_key?(:default_operator)
|
89
122
|
end
|
90
123
|
|
91
124
|
# Store this field is the field array
|
92
|
-
definition.fields[@field]
|
93
|
-
definition.
|
125
|
+
definition.fields[@field] ||= self unless options[:rename]
|
126
|
+
definition.fields[options[:rename].to_sym] ||= self if options[:rename]
|
127
|
+
definition.unique_fields << self
|
94
128
|
|
95
129
|
# Store definition for alias / aliases as well
|
96
|
-
definition.fields[options[:alias]]
|
97
|
-
options[:aliases].each { |al| definition.fields[al] ||= self } if options[:aliases]
|
130
|
+
definition.fields[options[:alias].to_sym] ||= self if options[:alias]
|
131
|
+
options[:aliases].each { |al| definition.fields[al.to_sym] ||= self } if options[:aliases]
|
98
132
|
end
|
99
133
|
end
|
100
134
|
|
@@ -110,10 +144,13 @@ module ScopedSearch
|
|
110
144
|
@profile_fields = {:default => {}}
|
111
145
|
@profile_unique_fields = {:default => []}
|
112
146
|
|
147
|
+
|
113
148
|
register_named_scope! unless klass.respond_to?(:search_for)
|
149
|
+
register_complete_for! unless klass.respond_to?(:complete_for)
|
150
|
+
|
114
151
|
end
|
115
|
-
|
116
|
-
attr_accessor :profile
|
152
|
+
|
153
|
+
attr_accessor :profile, :default_order
|
117
154
|
|
118
155
|
def fields
|
119
156
|
@profile ||= :default
|
@@ -125,6 +162,25 @@ module ScopedSearch
|
|
125
162
|
@profile_unique_fields[@profile] ||= []
|
126
163
|
end
|
127
164
|
|
165
|
+
# this method return definitions::field object from string
|
166
|
+
def field_by_name(name)
|
167
|
+
field = fields[name.to_sym]
|
168
|
+
field ||= fields[name.to_s.split('.')[0].to_sym]
|
169
|
+
field
|
170
|
+
end
|
171
|
+
|
172
|
+
# this method is used by the syntax auto completer to suggest operators.
|
173
|
+
def operator_by_field_name(name)
|
174
|
+
field = field_by_name(name)
|
175
|
+
return [] if field.nil?
|
176
|
+
return field.operators if field.operators
|
177
|
+
return ['= ', '!= '] if field.set?
|
178
|
+
return ['= ', '> ', '< ', '<= ', '>= ','!= '] if field.numerical?
|
179
|
+
return ['= ', '!= ', '~ ', '!~ '] if field.textual?
|
180
|
+
return ['= ', '> ', '< '] if field.temporal?
|
181
|
+
raise ScopedSearch::QueryNotSupported, "could not verify '#{name}' type, this can be a result of a definition error"
|
182
|
+
end
|
183
|
+
|
128
184
|
NUMERICAL_REGXP = /^\-?\d+(\.\d+)?$/
|
129
185
|
|
130
186
|
# Returns a list of appropriate fields to search in given a search keyword and operator.
|
@@ -133,9 +189,20 @@ module ScopedSearch
|
|
133
189
|
column_types = []
|
134
190
|
column_types += [:string, :text] if [nil, :like, :unlike, :ne, :eq].include?(operator)
|
135
191
|
column_types += [:integer, :double, :float, :decimal] if value =~ NUMERICAL_REGXP
|
136
|
-
column_types += [:datetime, :date, :timestamp] if (
|
192
|
+
column_types += [:datetime, :date, :timestamp] if (parse_temporal(value))
|
137
193
|
|
138
|
-
default_fields.select { |field| column_types.include?(field.type) }
|
194
|
+
default_fields.select { |field| column_types.include?(field.type) && !field.set? }
|
195
|
+
end
|
196
|
+
|
197
|
+
# Try to parse a string as a datetime.
|
198
|
+
# Supported formats are Today, Yesterday, Sunday, '1 day ago', '2 hours ago', '3 months ago','Jan 23, 2004'
|
199
|
+
# And many more formats that are documented in Ruby DateTime API Doc.
|
200
|
+
def parse_temporal(value)
|
201
|
+
return Date.current if value =~ /\btoday\b/i
|
202
|
+
return 1.day.ago.to_date if value =~ /\byesterday\b/i
|
203
|
+
return (eval(value.strip.gsub(/\s+/,'.').downcase)).to_datetime if value =~ /\A\s*\d+\s+\bhours?|minutes?\b\s+\bago\b\s*\z/i
|
204
|
+
return (eval(value.strip.gsub(/\s+/,'.').downcase)).to_date if value =~ /\A\s*\d+\s+\b(days?|weeks?|months?|years?)\b\s+\bago\b\s*\z/i
|
205
|
+
DateTime.parse(value, true) rescue nil
|
139
206
|
end
|
140
207
|
|
141
208
|
# Returns a list of fields that should be searched on by default.
|
@@ -165,6 +232,9 @@ module ScopedSearch
|
|
165
232
|
search_scope = @klass.scoped
|
166
233
|
search_scope = search_scope.where(find_options[:conditions]) if find_options[:conditions]
|
167
234
|
search_scope = search_scope.includes(find_options[:include]) if find_options[:include]
|
235
|
+
search_scope = search_scope.joins(find_options[:joins]) if find_options[:joins]
|
236
|
+
search_scope = search_scope.order(find_options[:order]) if find_options[:order]
|
237
|
+
search_scope = search_scope.group(find_options[:group]) if find_options[:group]
|
168
238
|
search_scope
|
169
239
|
})
|
170
240
|
else
|
@@ -176,3 +246,13 @@ module ScopedSearch
|
|
176
246
|
end
|
177
247
|
end
|
178
248
|
end
|
249
|
+
|
250
|
+
# Registers the complete_for method within the class that is used for searching.
|
251
|
+
def register_complete_for! # :nodoc
|
252
|
+
@klass.class_eval do
|
253
|
+
def self.complete_for (query)
|
254
|
+
search_options = ScopedSearch::AutoCompleteBuilder.auto_complete(@scoped_search , query)
|
255
|
+
search_options
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|