scoped_search 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|