acts_as_indexed 0.6.6 → 0.6.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +5 -0
- data/README.rdoc +44 -13
- data/VERSION +1 -1
- data/acts_as_indexed.gemspec +3 -2
- data/lib/acts_as_indexed/search_atom.rb +6 -0
- data/lib/acts_as_indexed/search_index.rb +45 -159
- data/lib/acts_as_indexed/storage.rb +136 -0
- data/lib/acts_as_indexed.rb +1 -4
- data/lib/will_paginate_search.rb +0 -2
- data/test/acts_as_indexed_test.rb +93 -111
- data/test/search_index_test.rb +2 -33
- metadata +5 -4
data/CHANGELOG
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
===0.6.7 [7th February 2011]
|
2
|
+
- find_by_index and paginate_search are no longer deprecated.
|
3
|
+
- Improved documentation
|
4
|
+
- Storage is now it's own class to allow future development of locking and pluggable backends.
|
5
|
+
|
1
6
|
===0.6.6 [31st August 2010]
|
2
7
|
- Now Heroku compatible out of the box, index is created in tmp when root dir is non-writable. [parndt - Philip Arndt - Great suggestion]
|
3
8
|
- Fixed a require path issue on 1.9.2.
|
data/README.rdoc
CHANGED
@@ -35,7 +35,7 @@ Gemfile (Rails 3.x.x).
|
|
35
35
|
|
36
36
|
==== No Git?
|
37
37
|
|
38
|
-
If you don't have git installed, you can download the plugin from the GitHub
|
38
|
+
If you don't have git installed, but still want the plugin, you can download the plugin from the GitHub
|
39
39
|
page (http://github.com/dougal/acts_as_indexed) and unpack it into the
|
40
40
|
<tt>vendor/plugins</tt> directory of your rails app.
|
41
41
|
|
@@ -85,8 +85,26 @@ or true if it is visible, you can filter the index by doing:
|
|
85
85
|
|
86
86
|
=== Searching
|
87
87
|
|
88
|
-
|
89
|
-
|
88
|
+
==== With Relevance
|
89
|
+
|
90
|
+
To search with the most relevant matches appearing first, call the
|
91
|
+
+find_with_index+ method on your model, passing a query as the first argument.
|
92
|
+
The optional +ids_only+ parameter, when set to true, will return only the IDs
|
93
|
+
of any matching records.
|
94
|
+
|
95
|
+
# Returns array of Post objects ordered by relevance.
|
96
|
+
my_search_results = Post.find_with_index('my search query')
|
97
|
+
|
98
|
+
# Pass any of the ActiveRecord find options to the search.
|
99
|
+
my_search_results = Post.find_with_index('my search query',{:limit => 10}) # return the first 10 matches.
|
100
|
+
|
101
|
+
# Returns array of IDs ordered by relevance.
|
102
|
+
my_search_results = Post.find_with_index('my search query',{},{:ids_only => true}) # => [12,19,33...
|
103
|
+
|
104
|
+
==== Without Relevance (Scope)
|
105
|
+
|
106
|
+
If the relevance of the results is not important, call the +with_query+ named
|
107
|
+
scope on your model, passing a query as an argument.
|
90
108
|
|
91
109
|
# Returns array of Post objects.
|
92
110
|
my_search_results = Post.with_query('my search query')
|
@@ -98,15 +116,23 @@ an argument.
|
|
98
116
|
|
99
117
|
The following query operators are supported:
|
100
118
|
|
101
|
-
* AND:: This is the default option. 'cat dog' will find records matching 'cat' AND 'dog'.
|
102
|
-
* NOT:: 'cat -dog' will find records matching 'cat' AND NOT 'dog'
|
103
|
-
* INCLUDE:: 'cat +me' will find records matching 'cat' and 'me', even if 'me' is smaller than the +min_word_size+
|
104
|
-
* "":: Quoted terms are matched as phrases. '"cat dog"' will find records matching the whole phrase. Quoted terms can be preceded by the NOT operator; 'cat -"big dog"' etc. Quoted terms can include words shorter than the +min_word_size+.
|
105
|
-
*
|
106
|
-
* ^"":: A quoted term that begins with ^ matches any phrase that begin with this phrase. '^"cat d"' will find records matching the whole phrases "cat dog" and "cat dinner". This type of search is useful for autocomplete inputs.
|
119
|
+
* AND :: This is the default option. 'cat dog' will find records matching 'cat' AND 'dog'.
|
120
|
+
* NOT :: 'cat -dog' will find records matching 'cat' AND NOT 'dog'
|
121
|
+
* INCLUDE :: 'cat +me' will find records matching 'cat' and 'me', even if 'me' is smaller than the +min_word_size+
|
122
|
+
* "" :: Quoted terms are matched as phrases. '"cat dog"' will find records matching the whole phrase. Quoted terms can be preceded by the NOT operator; 'cat -"big dog"' etc. Quoted terms can include words shorter than the +min_word_size+.
|
123
|
+
* ^ :: Terms that begin with ^ will match records that contain a word starting with the term. '^cat' will find matches containing 'cat', 'catapult', 'caterpillar' etc.
|
124
|
+
* ^"" :: A quoted term that begins with ^ matches any phrase that begin with this phrase. '^"cat d"' will find records matching the whole phrases "cat dog" and "cat dinner". This type of search is useful for autocomplete inputs.
|
107
125
|
|
108
126
|
=== Pagination
|
109
127
|
|
128
|
+
==== With Relevance
|
129
|
+
|
130
|
+
Pagination is supported via the +paginate_search+ method whose first argument is the search query, followed all the standard will_paginate arguments.
|
131
|
+
|
132
|
+
@images = Image.paginate_search('girl', :page => 1, :per_page => 5)
|
133
|
+
|
134
|
+
==== Without Relevance (Scope)
|
135
|
+
|
110
136
|
Since +with_query+ is a named scope, WillPaginate can be used in the normal
|
111
137
|
fashion.
|
112
138
|
|
@@ -118,7 +144,7 @@ A config block can be provided in your environment files or initializers.
|
|
118
144
|
Example showing defaults:
|
119
145
|
|
120
146
|
ActsAsIndexed.configure do |config|
|
121
|
-
config.index_file = [
|
147
|
+
config.index_file = [Rails.root.to_s,'index']
|
122
148
|
config.index_file_depth = 3
|
123
149
|
config.min_word_size = 3
|
124
150
|
end
|
@@ -146,10 +172,15 @@ online[http://rdoc.info/projects/dougal/acts_as_indexed/].
|
|
146
172
|
All of the above are most welcome. mailto:dougal.s@gmail.com
|
147
173
|
|
148
174
|
|
149
|
-
==
|
150
|
-
|
151
|
-
Douglas F Shearer - http://douglasfshearer.com
|
175
|
+
== Contributors
|
152
176
|
|
177
|
+
* Douglas F Shearer - http://douglasfshearer.com
|
178
|
+
* Thomas Pomfret
|
179
|
+
* Philip Arndt
|
180
|
+
* Fernanda Lopes
|
181
|
+
* Alex Coles
|
182
|
+
* Myles Eftos
|
183
|
+
* Edward Anderson
|
153
184
|
|
154
185
|
== Future Releases
|
155
186
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.6.
|
1
|
+
0.6.7
|
data/acts_as_indexed.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{acts_as_indexed}
|
8
|
-
s.version = "0.6.
|
8
|
+
s.version = "0.6.7"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Douglas F Shearer"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2011-02-07}
|
13
13
|
s.description = %q{Acts As Indexed is a plugin which provides a pain-free way to add fulltext search to your Ruby on Rails app}
|
14
14
|
s.email = %q{dougal.s@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -29,6 +29,7 @@ Gem::Specification.new do |s|
|
|
29
29
|
"lib/acts_as_indexed/configuration.rb",
|
30
30
|
"lib/acts_as_indexed/search_atom.rb",
|
31
31
|
"lib/acts_as_indexed/search_index.rb",
|
32
|
+
"lib/acts_as_indexed/storage.rb",
|
32
33
|
"lib/will_paginate_search.rb",
|
33
34
|
"rails/init.rb",
|
34
35
|
"test/abstract_unit.rb",
|
@@ -58,6 +58,12 @@ module ActsAsIndexed #:nodoc:
|
|
58
58
|
})
|
59
59
|
end
|
60
60
|
|
61
|
+
# Creates a new SearchAtom with records in other removed from self.
|
62
|
+
def -(other)
|
63
|
+
records = @records.clone.reject { |name, records| other.records.include?(name) }
|
64
|
+
SearchAtom.new(records)
|
65
|
+
end
|
66
|
+
|
61
67
|
# Returns at atom containing the records and positions of +self+ preceded by +former+
|
62
68
|
# "former latter" or "big dog" where "big" is the former and "dog" is the latter.
|
63
69
|
def preceded_by(former)
|
@@ -12,42 +12,37 @@ module ActsAsIndexed #:nodoc:
|
|
12
12
|
# min_word_size:: Smallest query term that will be run through search.
|
13
13
|
# if_proc:: A Proc. If the proc is true, the index gets added, if false if doesn't
|
14
14
|
def initialize(root, index_depth, fields, min_word_size, if_proc=Proc.new{true})
|
15
|
-
@
|
15
|
+
@storage = Storage.new(Pathname.new(root.to_s), index_depth)
|
16
16
|
@fields = fields
|
17
|
-
@index_depth = index_depth
|
18
17
|
@atoms = {}
|
19
18
|
@min_word_size = min_word_size
|
20
|
-
@records_size =
|
19
|
+
@records_size = @storage.record_count
|
21
20
|
@if_proc = if_proc
|
22
21
|
end
|
23
22
|
|
24
23
|
# Adds +record+ to the index.
|
25
|
-
def add_record(record
|
24
|
+
def add_record(record)
|
26
25
|
return unless @if_proc.call(record)
|
26
|
+
|
27
27
|
condensed_record = condense_record(record)
|
28
|
-
|
29
|
-
|
30
|
-
@
|
31
|
-
self.save unless no_save
|
28
|
+
atoms = add_occurences(condensed_record,record.id)
|
29
|
+
|
30
|
+
@storage.add(atoms)
|
32
31
|
end
|
33
32
|
|
34
33
|
# Adds multiple records to the index. Accepts an array of +records+.
|
35
34
|
def add_records(records)
|
36
35
|
records.each do |record|
|
37
|
-
add_record(record
|
36
|
+
add_record(record)
|
38
37
|
end
|
39
|
-
self.save
|
40
38
|
end
|
41
39
|
|
42
40
|
# Removes +record+ from the index.
|
43
41
|
def remove_record(record)
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
@records_size -= 1
|
49
|
-
end
|
50
|
-
self.save
|
42
|
+
condensed_record = condense_record(record)
|
43
|
+
atoms = add_occurences(condensed_record,record.id)
|
44
|
+
|
45
|
+
@storage.remove(atoms)
|
51
46
|
end
|
52
47
|
|
53
48
|
def update_record(record_new, record_old)
|
@@ -55,33 +50,11 @@ module ActsAsIndexed #:nodoc:
|
|
55
50
|
add_record(record_new)
|
56
51
|
end
|
57
52
|
|
58
|
-
# Saves the current index partitions to the filesystem.
|
59
|
-
def save
|
60
|
-
prepare
|
61
|
-
atoms_sorted = {}
|
62
|
-
@atoms.each do |atom_name, records|
|
63
|
-
(atoms_sorted[encoded_prefix(atom_name)] ||= {})[atom_name] = records
|
64
|
-
end
|
65
|
-
atoms_sorted.each do |e_p, atoms|
|
66
|
-
@root.join(e_p.to_s).open("w+") do |f|
|
67
|
-
Marshal.dump(atoms,f)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
save_record_size
|
71
|
-
end
|
72
|
-
|
73
|
-
# Deletes the current model's index from the filesystem.
|
74
|
-
#--
|
75
|
-
# TODO: Write a public method that will delete all indexes.
|
76
|
-
def destroy
|
77
|
-
@root.delete
|
78
|
-
end
|
79
|
-
|
80
53
|
# Returns an array of IDs for records matching +query+.
|
81
54
|
def search(query)
|
82
55
|
return [] if query.nil?
|
83
|
-
|
84
|
-
|
56
|
+
|
57
|
+
@atoms = @storage.fetch(cleanup_atoms(query), query[/\^/])
|
85
58
|
queries = parse_query(query.dup)
|
86
59
|
positive = run_queries(queries[:positive])
|
87
60
|
positive_quoted = run_quoted_queries(queries[:positive_quoted])
|
@@ -95,15 +68,15 @@ module ActsAsIndexed #:nodoc:
|
|
95
68
|
if queries[:start_quoted].any?
|
96
69
|
results = merge_query_results(results, start_quoted)
|
97
70
|
end
|
98
|
-
|
71
|
+
|
99
72
|
if queries[:starts_with].any?
|
100
73
|
results = merge_query_results(results, starts_with)
|
101
74
|
end
|
102
|
-
|
75
|
+
|
103
76
|
if queries[:positive_quoted].any?
|
104
77
|
results = merge_query_results(results, positive_quoted)
|
105
78
|
end
|
106
|
-
|
79
|
+
|
107
80
|
if queries[:positive].any?
|
108
81
|
results = merge_query_results(results, positive)
|
109
82
|
end
|
@@ -112,100 +85,32 @@ module ActsAsIndexed #:nodoc:
|
|
112
85
|
results.delete_if { |r_id, w| negative_results.include?(r_id) }
|
113
86
|
results
|
114
87
|
end
|
115
|
-
|
88
|
+
|
89
|
+
private
|
90
|
+
|
116
91
|
def merge_query_results(results1, results2)
|
117
92
|
# Return the other if one is empty.
|
118
93
|
return results1 if results2.empty?
|
119
94
|
return results2 if results1.empty?
|
120
|
-
|
95
|
+
|
121
96
|
# Delete any records from results 1 that are not in results 2.
|
122
97
|
r1 = results1.delete_if{ |r_id,w| !results2.include?(r_id) }
|
123
|
-
|
124
|
-
|
98
|
+
|
99
|
+
|
125
100
|
# Delete any records from results 2 that are not in results 1.
|
126
101
|
r2 = results2.delete_if{ |r_id,w| !results1.include?(r_id) }
|
127
|
-
|
102
|
+
|
128
103
|
# Merge the results by adding their respective scores.
|
129
104
|
r1.merge(r2) { |r_id,old_val,new_val| old_val + new_val}
|
130
105
|
end
|
131
|
-
|
132
|
-
# Returns true if the index root exists on the FS.
|
133
|
-
#--
|
134
|
-
# TODO: Make a private method called 'root_exists?' which checks for the root directory.
|
135
|
-
def exists?
|
136
|
-
@root.join('size').exist?
|
137
|
-
end
|
138
|
-
|
139
|
-
private
|
140
|
-
|
141
|
-
# Gets the size file from the index.
|
142
|
-
def load_record_size
|
143
|
-
@root.join('size').open do |f|
|
144
|
-
Marshal.load(f)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
# Saves the size to the size file.
|
149
|
-
def save_record_size
|
150
|
-
@root.join('size').open('w+') do |f|
|
151
|
-
Marshal.dump(@records_size,f)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
# Returns true if the given atom is present.
|
156
|
-
def include_atom?(atom)
|
157
|
-
if atom.is_a? Regexp
|
158
|
-
@atoms.keys.grep(atom).any?
|
159
|
-
else
|
160
|
-
@atoms.has_key?(atom)
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
# Returns true if all the given atoms are present.
|
165
|
-
def include_atoms?(atoms_arr)
|
166
|
-
atoms_arr.each do |a|
|
167
|
-
return false unless include_atom?(a)
|
168
|
-
end
|
169
|
-
true
|
170
|
-
end
|
171
|
-
|
172
|
-
# Returns true if the given record is present.
|
173
|
-
def include_record?(record_id)
|
174
|
-
@atoms.each do |atomname, atom|
|
175
|
-
return true if atom.include_record?(record_id)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
def add_atom(atom)
|
180
|
-
@atoms[atom] = SearchAtom.new unless include_atom?(atom)
|
181
|
-
end
|
182
106
|
|
183
107
|
def add_occurences(condensed_record,record_id)
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
end
|
189
|
-
|
190
|
-
def encoded_prefix(atom)
|
191
|
-
prefix = atom[0,@index_depth]
|
192
|
-
unless (@prefix_cache ||= {}).has_key?(prefix)
|
193
|
-
if atom.length > 1
|
194
|
-
@prefix_cache[prefix] = prefix.split(//).map{|c| encode_character(c)}.join('_')
|
195
|
-
else
|
196
|
-
@prefix_cache[prefix] = encode_character(atom)
|
197
|
-
end
|
198
|
-
end
|
199
|
-
@prefix_cache[prefix]
|
200
|
-
end
|
201
|
-
|
202
|
-
# Allows compatibility with 1.8.6 which has no ord method.
|
203
|
-
def encode_character(char)
|
204
|
-
if @@has_ord ||= char.respond_to?(:ord)
|
205
|
-
char.ord.to_s
|
206
|
-
else
|
207
|
-
char[0]
|
108
|
+
atoms = {}
|
109
|
+
condensed_record.each_with_index do |atom_name, i|
|
110
|
+
atoms[atom_name] = SearchAtom.new unless atoms.include?(atom_name)
|
111
|
+
atoms[atom_name].add_position(record_id, i)
|
208
112
|
end
|
113
|
+
atoms
|
209
114
|
end
|
210
115
|
|
211
116
|
def parse_query(s)
|
@@ -254,14 +159,15 @@ module ActsAsIndexed #:nodoc:
|
|
254
159
|
:positive_quoted => positive_quoted,
|
255
160
|
:starts_with => starts_with,
|
256
161
|
:negative => negative,
|
257
|
-
:positive => positive
|
162
|
+
:positive => positive
|
163
|
+
}
|
258
164
|
end
|
259
|
-
|
165
|
+
|
260
166
|
def run_queries(atoms, starts_with=false)
|
261
167
|
results = {}
|
262
168
|
atoms.each do |atom|
|
263
169
|
interim_results = {}
|
264
|
-
|
170
|
+
|
265
171
|
# If these atoms are to be run as 'starts with', make them a Regexp
|
266
172
|
# with a carat.
|
267
173
|
atom = /^#{atom}/ if starts_with
|
@@ -269,37 +175,37 @@ module ActsAsIndexed #:nodoc:
|
|
269
175
|
# Get the resulting matches, and break if none exist.
|
270
176
|
matches = get_atom_results(@atoms.keys, atom)
|
271
177
|
break if matches.nil?
|
272
|
-
|
178
|
+
|
273
179
|
# Grab the record IDs and weightings.
|
274
180
|
interim_results = matches.weightings(@records_size)
|
275
|
-
|
181
|
+
|
276
182
|
# Merge them with the results obtained already, if any.
|
277
183
|
results = results.empty? ? interim_results : merge_query_results(results, interim_results)
|
278
|
-
|
184
|
+
|
279
185
|
break if results.empty?
|
280
|
-
|
186
|
+
|
281
187
|
end
|
282
188
|
results
|
283
189
|
end
|
284
|
-
|
190
|
+
|
285
191
|
def run_quoted_queries(quoted_atoms, starts_with=false)
|
286
192
|
results = {}
|
287
193
|
quoted_atoms.each do |quoted_atom|
|
288
194
|
interim_results = {}
|
289
|
-
|
195
|
+
|
290
196
|
break if quoted_atom.empty?
|
291
|
-
|
197
|
+
|
292
198
|
# If these atoms are to be run as 'starts with', make the final atom a
|
293
199
|
# Regexp with a line-start anchor.
|
294
200
|
quoted_atom[-1] = /^#{quoted_atom.last}/ if starts_with
|
295
|
-
|
201
|
+
|
296
202
|
# Little bit of memoization.
|
297
203
|
atoms_keys = @atoms.keys
|
298
|
-
|
204
|
+
|
299
205
|
# Get the matches for the first atom.
|
300
206
|
matches = get_atom_results(atoms_keys, quoted_atom.first)
|
301
207
|
break if matches.nil?
|
302
|
-
|
208
|
+
|
303
209
|
# Check the index contains all the required atoms.
|
304
210
|
# for each of the others
|
305
211
|
# return atom containing records + positions where current atom is preceded by following atom.
|
@@ -320,9 +226,9 @@ module ActsAsIndexed #:nodoc:
|
|
320
226
|
|
321
227
|
# Merge them with the results obtained already, if any.
|
322
228
|
results = results.empty? ? interim_results : merge_query_results(results, interim_results)
|
323
|
-
|
229
|
+
|
324
230
|
break if results.empty?
|
325
|
-
|
231
|
+
|
326
232
|
end
|
327
233
|
results
|
328
234
|
end
|
@@ -340,26 +246,6 @@ module ActsAsIndexed #:nodoc:
|
|
340
246
|
end
|
341
247
|
end
|
342
248
|
|
343
|
-
def load_atoms(atoms, options={})
|
344
|
-
# Remove duplicate atoms.
|
345
|
-
# Remove atoms already in index.
|
346
|
-
# Calculate prefixes.
|
347
|
-
# Remove duplicate prefixes.
|
348
|
-
atoms.uniq.reject{|a| include_atom?(a)}.collect{|a| encoded_prefix(a)}.uniq.each do |name|
|
349
|
-
pattern = @root.join(name.to_s).to_s
|
350
|
-
pattern += '*' if options[:start]
|
351
|
-
Pathname.glob(pattern).each do |atom_file|
|
352
|
-
atom_file.open do |f|
|
353
|
-
@atoms.merge!(Marshal.load(f))
|
354
|
-
end
|
355
|
-
end
|
356
|
-
end
|
357
|
-
end
|
358
|
-
|
359
|
-
def prepare
|
360
|
-
# Makes the RAILS_ROOT/index/ENVIRONMENT/CLASS directories
|
361
|
-
@root.mkpath
|
362
|
-
end
|
363
249
|
|
364
250
|
def cleanup_atoms(s, limit_size=false, min_size = @min_word_size || 3)
|
365
251
|
atoms = s.downcase.gsub(/\W/,' ').squeeze(' ').split
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# ActsAsIndexed
|
2
|
+
# Copyright (c) 2007 - 2010 Douglas F Shearer.
|
3
|
+
# http://douglasfshearer.com
|
4
|
+
# Distributed under the MIT license as included with this plugin.
|
5
|
+
|
6
|
+
module ActsAsIndexed #:nodoc:
|
7
|
+
class Storage
|
8
|
+
|
9
|
+
def initialize(path, prefix_size)
|
10
|
+
@path = path
|
11
|
+
@prefix_size = prefix_size
|
12
|
+
prepare
|
13
|
+
end
|
14
|
+
|
15
|
+
# Takes a hash of atoms and adds these to storage.
|
16
|
+
def add(atoms)
|
17
|
+
operate(:+, atoms)
|
18
|
+
|
19
|
+
update_record_count(1)
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
# Takes a hash of atoms and removes these from storage.
|
24
|
+
def remove(atoms)
|
25
|
+
operate(:-, atoms)
|
26
|
+
|
27
|
+
update_record_count(-1)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Takes a string array of atoms names
|
31
|
+
# return a hash of the relevant atoms.
|
32
|
+
def fetch(atom_names, start=false)
|
33
|
+
atoms = {}
|
34
|
+
|
35
|
+
atom_names.uniq.collect{|a| encoded_prefix(a) }.uniq.each do |prefix|
|
36
|
+
pattern = @path.join(prefix.to_s).to_s
|
37
|
+
pattern += '*' if start
|
38
|
+
|
39
|
+
Pathname.glob(pattern).each do |atom_file|
|
40
|
+
atom_file.open do |f|
|
41
|
+
atoms.merge!(Marshal.load(f))
|
42
|
+
end
|
43
|
+
end # Pathname.glob
|
44
|
+
|
45
|
+
end # atom_names.uniq
|
46
|
+
atoms
|
47
|
+
end # fetch.
|
48
|
+
|
49
|
+
# Returns the number of records currently stored in this index.
|
50
|
+
def record_count
|
51
|
+
# TODO: Record count is currently a marshaled integer. Why not store as
|
52
|
+
# string integer? Breaks compatibility, so leave until other changes
|
53
|
+
# need to be made to the index.
|
54
|
+
|
55
|
+
@path.join('size').open do |f|
|
56
|
+
Marshal.load(f)
|
57
|
+
end
|
58
|
+
|
59
|
+
# This is a bit horrible.
|
60
|
+
rescue Errno::ENOENT
|
61
|
+
0
|
62
|
+
rescue EOFError
|
63
|
+
0
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# Takes atoms and adds or removes them from the index depending on the
|
69
|
+
# passed operation.
|
70
|
+
def operate(operation, atoms)
|
71
|
+
# ActiveSupport always available?
|
72
|
+
atoms_sorted = ActiveSupport::OrderedHash.new
|
73
|
+
|
74
|
+
# Sort the atoms into the appropriate shards for writing to individual
|
75
|
+
# files.
|
76
|
+
atoms.each do |atom_name, records|
|
77
|
+
(atoms_sorted[encoded_prefix(atom_name)] ||= {})[atom_name] = records
|
78
|
+
end
|
79
|
+
|
80
|
+
atoms_sorted.each do |e_p, atoms|
|
81
|
+
path = @path.join(e_p.to_s)
|
82
|
+
|
83
|
+
if path.exist?
|
84
|
+
from_file = path.open do |f|
|
85
|
+
Marshal.load(f)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
from_file = {}
|
89
|
+
end
|
90
|
+
|
91
|
+
atoms = from_file.merge(atoms){ |k,o,n| o.send(operation, n) }
|
92
|
+
|
93
|
+
path.open("w+") do |f|
|
94
|
+
Marshal.dump(atoms,f)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def update_record_count(delta)
|
100
|
+
new_count = self.record_count + delta
|
101
|
+
new_count = 0 if new_count < 0
|
102
|
+
|
103
|
+
@path.join('size').open('w+') do |f|
|
104
|
+
Marshal.dump(new_count,f)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def prepare
|
109
|
+
@path.mkpath unless @path.exist?
|
110
|
+
end
|
111
|
+
|
112
|
+
def encoded_prefix(atom)
|
113
|
+
prefix = atom[0, @prefix_size]
|
114
|
+
|
115
|
+
unless (@prefix_cache ||= {}).has_key?(prefix)
|
116
|
+
if atom.length > 1
|
117
|
+
@prefix_cache[prefix] = prefix.split(//).map{|c| encode_character(c)}.join('_')
|
118
|
+
else
|
119
|
+
@prefix_cache[prefix] = encode_character(atom)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
@prefix_cache[prefix]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Allows compatibility with 1.8.6 which has no ord method.
|
127
|
+
def encode_character(char)
|
128
|
+
if @@has_ord ||= char.respond_to?(:ord)
|
129
|
+
char.ord.to_s
|
130
|
+
else
|
131
|
+
char[0]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
data/lib/acts_as_indexed.rb
CHANGED
@@ -8,6 +8,7 @@ require 'active_record'
|
|
8
8
|
require 'acts_as_indexed/configuration'
|
9
9
|
require 'acts_as_indexed/search_index'
|
10
10
|
require 'acts_as_indexed/search_atom'
|
11
|
+
require 'acts_as_indexed/storage'
|
11
12
|
|
12
13
|
module ActsAsIndexed #:nodoc:
|
13
14
|
|
@@ -102,8 +103,6 @@ module ActsAsIndexed #:nodoc:
|
|
102
103
|
|
103
104
|
def index_remove(record)
|
104
105
|
index = SearchIndex.new(aai_config.index_file, aai_config.index_file_depth, aai_fields, aai_config.min_word_size, aai_config.if_proc)
|
105
|
-
# record won't be in index if it doesn't exist. Just return true.
|
106
|
-
return unless index.exists?
|
107
106
|
index.remove_record(record)
|
108
107
|
@query_cache = {}
|
109
108
|
end
|
@@ -192,12 +191,10 @@ module ActsAsIndexed #:nodoc:
|
|
192
191
|
# Adds model class singleton methods.
|
193
192
|
module SingletonMethods
|
194
193
|
|
195
|
-
# DEPRECATED. Use +with_query+ scope instead.
|
196
194
|
# Finds instances matching the terms passed in +query+.
|
197
195
|
#
|
198
196
|
# See ActsAsIndexed::ClassMethods#search_index.
|
199
197
|
def find_with_index(query='', find_options = {}, options = {})
|
200
|
-
warn "[DEPRECATION] `find_with_index` is deprecated and will be removed in a later release. Use `with_query(query)` instead."
|
201
198
|
search_index(query, find_options, options)
|
202
199
|
end
|
203
200
|
|
data/lib/will_paginate_search.rb
CHANGED
@@ -8,9 +8,7 @@ module ActsAsIndexed
|
|
8
8
|
|
9
9
|
module Search
|
10
10
|
|
11
|
-
# DEPRECATED. Use chained pagination instead.
|
12
11
|
def paginate_search(query, options)
|
13
|
-
warn "[DEPRECATION] `paginate_search` is deprecated and will be removed in a later release. Use `with_query(query).paginate()` instead."
|
14
12
|
page, per_page, total_entries = wp_parse_options(options)
|
15
13
|
|
16
14
|
total_entries ||= find_with_index(query,{},{:ids_only => true}).size
|
@@ -14,164 +14,136 @@ class ActsAsIndexedTest < ActiveSupport::TestCase
|
|
14
14
|
assert_equal [], Post.find_with_index('badger')
|
15
15
|
post = Post.new(:title => 'badger', :body => 'Thousands of them!')
|
16
16
|
assert post.save
|
17
|
-
assert_equal original_post_count+1, Post.count
|
18
|
-
assert_equal [post.id],
|
17
|
+
assert_equal original_post_count + 1, Post.count
|
18
|
+
assert_equal [post.id], result_ids('badger')
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
def test_removes_from_index
|
22
22
|
original_post_count = Post.count
|
23
|
-
assert_equal [posts(:wikipedia_article_4).id],
|
23
|
+
assert_equal [posts(:wikipedia_article_4).id], result_ids('album')
|
24
24
|
assert Post.find(posts(:wikipedia_article_4).id).destroy
|
25
|
-
assert_equal [],
|
26
|
-
assert_equal original_post_count-1, Post.count
|
25
|
+
assert_equal [], result_ids('album')
|
26
|
+
assert_equal original_post_count - 1, Post.count
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
def test_search_returns_posts
|
30
|
-
Post.find_with_index('album').each do |p|
|
31
|
-
assert_equal Post, p.class
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def test_scoped_search_returns_posts
|
36
30
|
Post.with_query('album').each do |p|
|
37
31
|
assert_equal Post, p.class
|
38
32
|
end
|
39
33
|
end
|
40
|
-
|
34
|
+
|
41
35
|
def test_search_returns_post_ids
|
42
|
-
|
36
|
+
result_ids('album').each do |pid|
|
43
37
|
assert p = Post.find(pid)
|
44
38
|
assert_equal Post, p.class
|
45
39
|
end
|
46
40
|
end
|
47
|
-
|
41
|
+
|
48
42
|
# After a portion of a record has been removed
|
49
43
|
# the portion removes should no longer be in the index.
|
50
44
|
def test_updates_index
|
51
45
|
p = Post.create(:title => 'A special title', :body => 'foo bar bla bla bla')
|
52
|
-
assert
|
46
|
+
assert result_ids('title').include?(p.id)
|
53
47
|
p.update_attributes(:title => 'No longer special')
|
54
|
-
assert !
|
48
|
+
assert !result_ids('title').include?(p.id)
|
55
49
|
end
|
56
|
-
|
50
|
+
|
57
51
|
def test_simple_queries
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
65
|
-
|
66
|
-
def test_scoped_simple_queries
|
67
|
-
assert_equal [], Post.find_with_index(nil)
|
68
|
-
assert_equal [], Post.with_query('')
|
69
|
-
assert_equal [5, 6], Post.with_query('ship').map{|r| r.id}.sort
|
70
|
-
assert_equal [6], Post.with_query('foo').map{|r| r.id}
|
71
|
-
assert_equal [6], Post.with_query('foo ship').map{|r| r.id}
|
72
|
-
assert_equal [6], Post.with_query('ship foo').map{|r| r.id}
|
73
|
-
end
|
74
|
-
|
52
|
+
assert_equal_array_contents [], result_ids(nil)
|
53
|
+
assert_equal_array_contents [], result_ids('')
|
54
|
+
assert_equal_array_contents [5, 6], result_ids('ship')
|
55
|
+
assert_equal_array_contents [6], result_ids('foo')
|
56
|
+
assert_equal_array_contents [6], result_ids('foo ship')
|
57
|
+
assert_equal_array_contents [6], result_ids('ship foo')
|
58
|
+
end
|
59
|
+
|
75
60
|
def test_negative_queries
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
81
|
-
|
82
|
-
def test_scoped_negative_queries
|
83
|
-
assert_equal [5, 6], Post.with_query('crane').map{|r| r.id}.sort
|
84
|
-
assert_equal [5], Post.with_query('crane -foo').map{|r| r.id}
|
85
|
-
assert_equal [5], Post.with_query('-foo crane').map{|r| r.id}
|
86
|
-
assert_equal [], Post.with_query('-foo') #Edgecase
|
61
|
+
assert_equal_array_contents [5, 6], result_ids('crane')
|
62
|
+
assert_equal_array_contents [5], result_ids('crane -foo')
|
63
|
+
assert_equal_array_contents [5], result_ids('-foo crane')
|
64
|
+
assert_equal_array_contents [], result_ids('-foo') #Edgecase
|
87
65
|
end
|
88
|
-
|
66
|
+
|
89
67
|
def test_quoted_queries
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
68
|
+
assert_equal_array_contents [5], result_ids('"crane ship"')
|
69
|
+
assert_equal_array_contents [6], result_ids('"crane big"')
|
70
|
+
assert_equal_array_contents [], result_ids('foo "crane ship"')
|
71
|
+
assert_equal_array_contents [], result_ids('"crane badger"')
|
94
72
|
end
|
95
|
-
|
96
|
-
def test_scoped_quoted_queries
|
97
|
-
assert_equal [5], Post.with_query('"crane ship"').map{|r| r.id}
|
98
|
-
assert_equal [6], Post.with_query('"crane big"').map{|r| r.id}
|
99
|
-
assert_equal [], Post.with_query('foo "crane ship"')
|
100
|
-
assert_equal [], Post.with_query('"crane badger"')
|
101
|
-
end
|
102
|
-
|
73
|
+
|
103
74
|
def test_negative_quoted_queries
|
104
|
-
|
105
|
-
|
75
|
+
assert_equal_array_contents [6], result_ids('crane -"crane ship"')
|
76
|
+
assert_equal_array_contents [], result_ids('-"crane big"') # Edgecase
|
106
77
|
end
|
107
|
-
|
78
|
+
|
108
79
|
def test_scoped_negative_quoted_queries
|
109
|
-
|
110
|
-
|
80
|
+
assert_equal_array_contents [6], result_ids('crane -"crane ship"')
|
81
|
+
assert_equal_array_contents [], result_ids('-"crane big"') # Edgecase
|
111
82
|
end
|
112
|
-
|
83
|
+
|
113
84
|
def test_start_queries
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
end
|
127
|
-
|
85
|
+
assert_equal_array_contents [6,5], result_ids('ship ^crane')
|
86
|
+
assert_equal_array_contents [6,5], result_ids('^crane ship')
|
87
|
+
assert_equal_array_contents [6,5], result_ids('^ship ^crane')
|
88
|
+
assert_equal_array_contents [6,5], result_ids('^crane ^ship')
|
89
|
+
assert_equal_array_contents [6,5], result_ids('^ship crane')
|
90
|
+
assert_equal_array_contents [6,5], result_ids('crane ^ship')
|
91
|
+
assert_equal_array_contents [6,5], result_ids('^crane')
|
92
|
+
assert_equal_array_contents [6,5], result_ids('^cran')
|
93
|
+
assert_equal_array_contents [6,5], result_ids('^cra')
|
94
|
+
assert_equal_array_contents [6,5,4], result_ids('^cr')
|
95
|
+
assert_equal_array_contents [6,5,4,3,2,1], result_ids('^c')
|
96
|
+
assert_equal_array_contents [], result_ids('^notthere')
|
97
|
+
end
|
98
|
+
|
128
99
|
def test_start_quoted_queries
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
142
|
-
|
100
|
+
assert_equal_array_contents [6,5], result_ids('^"crane" ship')
|
101
|
+
assert_equal_array_contents [6,5], result_ids('ship ^"crane"')
|
102
|
+
assert_equal_array_contents [5], result_ids('^"crane ship"')
|
103
|
+
assert_equal_array_contents [5], result_ids('^"crane shi"')
|
104
|
+
assert_equal_array_contents [5], result_ids('^"crane sh"')
|
105
|
+
assert_equal_array_contents [5], result_ids('^"crane s"')
|
106
|
+
assert_equal_array_contents [6,5], result_ids('^"crane "')
|
107
|
+
assert_equal_array_contents [6,5], result_ids('^"crane"')
|
108
|
+
assert_equal_array_contents [6,5], result_ids('^"cran"')
|
109
|
+
assert_equal_array_contents [6,5], result_ids('^"cra"')
|
110
|
+
assert_equal_array_contents [6,5,4], result_ids('^"cr"')
|
111
|
+
assert_equal_array_contents [6,5,4,3,2,1], result_ids('^"c"')
|
112
|
+
end
|
113
|
+
|
143
114
|
def test_find_options
|
144
115
|
all_results = Post.find_with_index('crane',{},{:ids_only => true})
|
116
|
+
assert_equal 2, all_results.size
|
117
|
+
|
145
118
|
first_result = Post.find_with_index('crane',{:limit => 1})
|
146
|
-
|
147
119
|
assert_equal 1, first_result.size
|
148
120
|
assert_equal all_results.first, first_result.first.id
|
149
|
-
|
121
|
+
|
150
122
|
second_result = Post.find_with_index('crane',{:limit => 1, :offset => 1})
|
151
123
|
assert_equal 1, second_result.size
|
152
124
|
assert_equal all_results[1], second_result.first.id
|
153
125
|
end
|
154
|
-
|
126
|
+
|
155
127
|
# When a atom already in a record is duplicated, it removes
|
156
128
|
# all records with that same atom from the index.
|
157
129
|
def test_update_record_bug
|
158
130
|
p = Post.find(6)
|
159
131
|
assert p.update_attributes(:body => p.body + ' crane')
|
160
|
-
assert_equal 2,
|
161
|
-
assert_equal 2,
|
132
|
+
assert_equal 2, result_ids('crane').size
|
133
|
+
assert_equal 2, result_ids('ship').size
|
162
134
|
end
|
163
|
-
|
135
|
+
|
164
136
|
# If an if proc is supplied, the index should only be created if the proc evaluated true
|
165
137
|
def test_create_if
|
166
138
|
Post.acts_as_indexed :fields => [:title, :body], :if => Proc.new { |post| post.visible }
|
167
|
-
|
139
|
+
|
168
140
|
original_post_count = Post.count
|
169
|
-
assert_equal [], Post.find_with_index('badger')
|
141
|
+
assert_equal [], Post.find_with_index('badger', {}, { :no_query_cache => true, :ids_only => true})
|
170
142
|
p = Post.new(:title => 'badger', :body => 'thousands of them!', :visible => true)
|
171
143
|
assert p.save
|
172
144
|
assert_equal original_post_count + 1, Post.count
|
173
145
|
assert_equal [p.id], Post.find_with_index('badger', {}, { :no_query_cache => true, :ids_only => true})
|
174
|
-
|
146
|
+
|
175
147
|
original_post_count = Post.count
|
176
148
|
assert_equal [], Post.find_with_index('unicorns')
|
177
149
|
p = Post.new(:title => 'unicorns', :body => 'there are none', :visible => false)
|
@@ -179,49 +151,59 @@ class ActsAsIndexedTest < ActiveSupport::TestCase
|
|
179
151
|
assert_equal original_post_count + 1, Post.count
|
180
152
|
assert_equal [], Post.find_with_index('unicorns', {}, { :no_query_cache => true, :ids_only => true})
|
181
153
|
end
|
182
|
-
|
154
|
+
|
183
155
|
# If an index already exists, and an if proc is supplied, and the proc is true, it should still appear in the index
|
184
156
|
def test_update_if_update
|
185
157
|
Post.acts_as_indexed :fields => [:title, :body], :if => Proc.new { |post| post.visible }
|
186
158
|
destroy_index
|
187
|
-
|
159
|
+
|
188
160
|
assert_equal 1, Post.find_with_index('crane', {}, { :no_query_cache => true, :ids_only => true}).size
|
189
161
|
p = Post.find(6)
|
190
162
|
assert p.update_attributes(:visible => true)
|
191
163
|
assert_equal 1, Post.find_with_index('crane', {}, { :no_query_cache => true, :ids_only => true}).size
|
192
164
|
end
|
193
|
-
|
165
|
+
|
194
166
|
# If an index already exists, and an if proc is supplied, and the proc is false, it should no longer appear in the index
|
195
167
|
def test_update_if_remove
|
196
168
|
Post.acts_as_indexed :fields => [:title, :body], :if => Proc.new { |post| post.visible }
|
197
169
|
destroy_index
|
198
|
-
|
170
|
+
|
199
171
|
assert_equal 1, Post.find_with_index('crane', {}, { :no_query_cache => true, :ids_only => true}).size
|
200
172
|
p = Post.find(6)
|
201
173
|
assert p.update_attributes(:visible => false)
|
202
174
|
assert_equal 0, Post.find_with_index('crane',{},{ :no_query_cache => true, :ids_only => true}).size
|
203
175
|
end
|
204
|
-
|
176
|
+
|
205
177
|
# If an index doesn't exist, and an if proc is supplied, and the proc is true, it should appear in the index
|
206
178
|
def test_update_if_add
|
207
179
|
Post.acts_as_indexed :fields => [:title, :body], :if => Proc.new { |post| post.visible }
|
208
180
|
destroy_index
|
209
|
-
|
181
|
+
|
210
182
|
assert_equal 1, Post.find_with_index('crane', {}, { :no_query_cache => true, :ids_only => true}).size
|
211
183
|
p = Post.find(5)
|
212
184
|
assert p.update_attributes(:visible => true)
|
213
185
|
assert_equal 2, Post.find_with_index('crane',{},{ :no_query_cache => true, :ids_only => true}).size
|
214
186
|
end
|
215
|
-
|
187
|
+
|
216
188
|
# If an index doesn't exist, and an if proc is supplied, and the proc is false, then nothing happens
|
217
189
|
def test_update_if_not_in
|
218
190
|
Post.acts_as_indexed :fields => [:title, :body], :if => Proc.new { |post| post.visible }
|
219
191
|
destroy_index
|
220
|
-
|
192
|
+
|
221
193
|
assert_equal 1, Post.find_with_index('crane', {}, { :no_query_cache => true, :ids_only => true}).size
|
222
194
|
p = Post.find(5)
|
223
195
|
assert p.update_attributes(:visible => false)
|
224
196
|
assert_equal 1, Post.find_with_index('crane',{},{ :no_query_cache => true, :ids_only => true}).size
|
225
197
|
end
|
226
198
|
|
199
|
+
private
|
200
|
+
|
201
|
+
def result_ids(query)
|
202
|
+
Post.with_query(query).map { |r| r.id }
|
203
|
+
end
|
204
|
+
|
205
|
+
def assert_equal_array_contents(a, b)
|
206
|
+
a.sort == b.sort
|
207
|
+
end
|
208
|
+
|
227
209
|
end
|
data/test/search_index_test.rb
CHANGED
@@ -6,40 +6,9 @@ class SearchIndexTest < ActiveSupport::TestCase
|
|
6
6
|
def teardown
|
7
7
|
destroy_index
|
8
8
|
end
|
9
|
-
|
10
|
-
def test_should_check_for_non_existing_index
|
11
|
-
SearchIndex.any_instance.expects(:exists?).returns(false)
|
12
|
-
File.expects(:open).never
|
13
|
-
assert build_search_index
|
14
|
-
end
|
15
|
-
|
16
|
-
def test_should_check_for_existing_index
|
17
|
-
SearchIndex.any_instance.expects(:exists?).returns(true)
|
18
|
-
SearchIndex.any_instance.expects(:load_record_size).returns(0)
|
19
|
-
assert build_search_index
|
20
|
-
end
|
21
9
|
|
22
|
-
|
23
|
-
|
24
|
-
mock_record = mock(:id => 123)
|
25
|
-
mock_condensed_record = ['mock','condensed','record']
|
26
|
-
|
27
|
-
search_index.expects(:condense_record).with(mock_record).returns(mock_condensed_record)
|
28
|
-
search_index.expects(:load_atoms).with(mock_condensed_record)
|
29
|
-
search_index.expects(:add_occurences).with(mock_condensed_record,123)
|
30
|
-
|
31
|
-
search_index.add_record(mock_record)
|
32
|
-
end
|
33
|
-
|
34
|
-
def test_add_records
|
35
|
-
search_index = build_search_index
|
36
|
-
mock_records = ['record0', 'record1']
|
37
|
-
|
38
|
-
search_index.expects(:add_record).with('record0', true)
|
39
|
-
search_index.expects(:add_record).with('record1', true)
|
40
|
-
|
41
|
-
search_index.add_records(mock_records)
|
42
|
-
end
|
10
|
+
# Write new tests for this since most of the existing tests were concerned
|
11
|
+
# with the storage routines which have now been moved elsewhere.
|
43
12
|
|
44
13
|
private
|
45
14
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_indexed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 9
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 6
|
9
|
-
-
|
10
|
-
version: 0.6.
|
9
|
+
- 7
|
10
|
+
version: 0.6.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Douglas F Shearer
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2011-02-07 00:00:00 +00:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -41,6 +41,7 @@ files:
|
|
41
41
|
- lib/acts_as_indexed/configuration.rb
|
42
42
|
- lib/acts_as_indexed/search_atom.rb
|
43
43
|
- lib/acts_as_indexed/search_index.rb
|
44
|
+
- lib/acts_as_indexed/storage.rb
|
44
45
|
- lib/will_paginate_search.rb
|
45
46
|
- rails/init.rb
|
46
47
|
- test/abstract_unit.rb
|