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 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
- To search, call the +with_query+ named scope on your model, passing a query as
89
- an argument.
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
- * ^:: Terms that begin with ^ will match records that contain a word starting with the term. '^cat' will find matches containing 'cat', 'catapult', 'caterpillar' etc.
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 = [RAILS_ROOT,'index']
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
- == Credits
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.6
1
+ 0.6.7
@@ -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.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{2010-08-31}
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
- @root = Pathname.new(root.to_s)
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 = exists? ? load_record_size : 0
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, no_save=false)
24
+ def add_record(record)
26
25
  return unless @if_proc.call(record)
26
+
27
27
  condensed_record = condense_record(record)
28
- load_atoms(condensed_record)
29
- add_occurences(condensed_record,record.id)
30
- @records_size += 1
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, true)
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
- atoms = condense_record(record)
45
- load_atoms(atoms)
46
- atoms.each do |a|
47
- @atoms[a].remove_record(record.id) if @atoms.has_key?(a)
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
- load_options = { :start => true } if query[/\^/]
84
- load_atoms(cleanup_atoms(query), load_options || {})
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
- condensed_record.each_with_index do |atom, i|
185
- add_atom(atom)
186
- @atoms[atom].add_position(record_id, i)
187
- end
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
@@ -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
 
@@ -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], Post.find_with_index('badger',{},{:ids_only => true})
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], Post.find_with_index('album',{},{:ids_only => true})
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 [], Post.find_with_index('album',{},{:ids_only => true})
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
- Post.find_with_index('album',{},{:ids_only => true}).each do |pid|
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 Post.find_with_index('title',{},{:ids_only => true}).include?(p.id)
46
+ assert result_ids('title').include?(p.id)
53
47
  p.update_attributes(:title => 'No longer special')
54
- assert !Post.find_with_index('title',{},{:ids_only => true}).include?(p.id)
48
+ assert !result_ids('title').include?(p.id)
55
49
  end
56
-
50
+
57
51
  def test_simple_queries
58
- assert_equal [], Post.find_with_index(nil)
59
- assert_equal [], Post.find_with_index('')
60
- assert_equal [5, 6], Post.find_with_index('ship',{},{:ids_only => true}).sort
61
- assert_equal [6], Post.find_with_index('foo',{},{:ids_only => true})
62
- assert_equal [6], Post.find_with_index('foo ship',{},{:ids_only => true})
63
- assert_equal [6], Post.find_with_index('ship foo',{},{:ids_only => true})
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
- assert_equal [5, 6], Post.find_with_index('crane',{},{:ids_only => true}).sort
77
- assert_equal [5], Post.find_with_index('crane -foo',{},{:ids_only => true})
78
- assert_equal [5], Post.find_with_index('-foo crane',{},{:ids_only => true})
79
- assert_equal [], Post.find_with_index('-foo') #Edgecase
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
- assert_equal [5], Post.find_with_index('"crane ship"',{},{:ids_only => true})
91
- assert_equal [6], Post.find_with_index('"crane big"',{},{:ids_only => true})
92
- assert_equal [], Post.find_with_index('foo "crane ship"')
93
- assert_equal [], Post.find_with_index('"crane badger"')
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
- assert_equal [6], Post.find_with_index('crane -"crane ship"',{},{:ids_only => true})
105
- assert_equal [], Post.find_with_index('-"crane big"',{},{:ids_only => true}) # Edgecase
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
- assert_equal [6], Post.with_query('crane -"crane ship"').map{|r| r.id}
110
- assert_equal [], Post.with_query('-"crane big"') # Edgecase
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
- assert_equal [6,5], Post.find_with_index('ship ^crane',{},{:ids_only => true})
115
- assert_equal [6,5], Post.find_with_index('^crane ship',{},{:ids_only => true})
116
- assert_equal [6,5], Post.find_with_index('^ship ^crane',{},{:ids_only => true})
117
- assert_equal [6,5], Post.find_with_index('^crane ^ship',{},{:ids_only => true})
118
- assert_equal [6,5], Post.find_with_index('^ship crane',{},{:ids_only => true})
119
- assert_equal [6,5], Post.find_with_index('crane ^ship',{},{:ids_only => true})
120
- assert_equal [6,5], Post.find_with_index('^crane',{},{:ids_only => true})
121
- assert_equal [6,5], Post.find_with_index('^cran',{},{:ids_only => true})
122
- assert_equal [6,5], Post.find_with_index('^cra',{},{:ids_only => true})
123
- assert_equal [6,5,4], Post.find_with_index('^cr',{},{:ids_only => true})
124
- assert_equal [6,5,4,3,2,1], Post.find_with_index('^c',{},{:ids_only => true})
125
- assert_equal [], Post.find_with_index('^notthere',{},{:ids_only => true})
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
- assert_equal [6,5], Post.find_with_index('^"crane" ship',{},{:ids_only => true})
130
- assert_equal [6,5], Post.find_with_index('ship ^"crane"',{},{:ids_only => true})
131
- assert_equal [5], Post.find_with_index('^"crane ship"',{},{:ids_only => true})
132
- assert_equal [5], Post.find_with_index('^"crane shi"',{},{:ids_only => true})
133
- assert_equal [5], Post.find_with_index('^"crane sh"',{},{:ids_only => true})
134
- assert_equal [5], Post.find_with_index('^"crane s"',{},{:ids_only => true})
135
- assert_equal [6,5], Post.find_with_index('^"crane "',{},{:ids_only => true})
136
- assert_equal [6,5], Post.find_with_index('^"crane"',{},{:ids_only => true})
137
- assert_equal [6,5], Post.find_with_index('^"cran"',{},{:ids_only => true})
138
- assert_equal [6,5], Post.find_with_index('^"cra"',{},{:ids_only => true})
139
- assert_equal [6,5,4], Post.find_with_index('^"cr"',{},{:ids_only => true})
140
- assert_equal [6,5,4,3,2,1], Post.find_with_index('^"c"',{},{:ids_only => true})
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, Post.find_with_index('crane',{},{:ids_only => true}).size
161
- assert_equal 2, Post.find_with_index('ship',{},{:ids_only => true}).size
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
@@ -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
- def test_add_record
23
- search_index = build_search_index
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: 11
4
+ hash: 9
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 6
9
- - 6
10
- version: 0.6.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: 2010-08-31 00:00:00 +01:00
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