acts_as_indexed 0.6.7 → 0.7.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/CHANGELOG CHANGED
@@ -1,7 +1,14 @@
1
+ ===0.7.0 [11th February 2011]
2
+ - Threadsafe support. Index files are now locked for changes, and atomically written.
3
+ - Configurable case-sensitivity.
4
+ - Improved performance of index builds.
5
+ - Now warns on old version of the index.
6
+ - Upgrade instructions added to README. [ionas - Florent Guilleux]
7
+
1
8
  ===0.6.7 [7th February 2011]
2
9
  - 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.
10
+ - Improved documentation.
11
+ - Storage is now its own class to allow future development of locking and pluggable backends.
5
12
 
6
13
  ===0.6.6 [31st August 2010]
7
14
  - Now Heroku compatible out of the box, index is created in tmp when root dir is non-writable. [parndt - Philip Arndt - Great suggestion]
@@ -86,15 +93,15 @@
86
93
 
87
94
  ===0.3.0 [18 September 2007]
88
95
  - Minor bug fixes.
89
- - min_word_size now works properly, with queries containing small words in
96
+ - min_word_size now works properly, with queries containing small words in
90
97
  quotes or being preceded by a '+' symbol are now searched on.
91
98
 
92
99
  ===0.2.2 [06 September 2007]
93
- - Search now caches query results within a session. Call the search twice in an
100
+ - Search now caches query results within a session. Call the search twice in an
94
101
  action? Only runs once!
95
102
 
96
103
  ===0.2.1 [05 September 2007]
97
- - AR find options can now be passed to the search to allow finer control of
104
+ - AR find options can now be passed to the search to allow finer control of
98
105
  returned Model Objects.
99
106
 
100
107
  ===0.2.0 [04 September 2007]
data/Gemfile CHANGED
@@ -3,3 +3,5 @@ source "http://rubygems.org"
3
3
  gem "jeweler"
4
4
  gem "mocha"
5
5
  gem "sqlite3-ruby"
6
+ gem "rcov"
7
+ gem "gemcutter"
@@ -1,19 +1,18 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
- gemcutter (0.6.1)
5
4
  git (1.2.5)
6
- jeweler (1.4.0)
7
- gemcutter (>= 0.1.0)
5
+ jeweler (1.5.2)
6
+ bundler (~> 1.0.0)
8
7
  git (>= 1.2.5)
9
- rubyforge (>= 2.0.0)
10
- json_pure (1.4.6)
11
- mocha (0.9.8)
8
+ rake
9
+ mocha (0.9.11)
12
10
  rake
13
11
  rake (0.8.7)
14
- rubyforge (2.0.4)
15
- json_pure (>= 1.1.7)
16
- sqlite3-ruby (1.3.1)
12
+ rcov (0.9.9)
13
+ sqlite3 (1.3.3)
14
+ sqlite3-ruby (1.3.3)
15
+ sqlite3 (>= 1.3.3)
17
16
 
18
17
  PLATFORMS
19
18
  ruby
@@ -21,4 +20,5 @@ PLATFORMS
21
20
  DEPENDENCIES
22
21
  jeweler
23
22
  mocha
23
+ rcov
24
24
  sqlite3-ruby
@@ -1,4 +1,4 @@
1
- Copyright (c) 2007 - 2010 Douglas Shearer
1
+ Copyright (c) 2007 - 2011 Douglas Shearer
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -35,9 +35,14 @@ Gemfile (Rails 3.x.x).
35
35
 
36
36
  ==== No Git?
37
37
 
38
- If you don't have git installed, but still want the plugin, you can download the plugin from the GitHub
39
- page (http://github.com/dougal/acts_as_indexed) and unpack it into the
40
- <tt>vendor/plugins</tt> directory of your rails app.
38
+ If you don't have git installed, but still want the plugin, you can download
39
+ the plugin from the GitHub page (http://github.com/dougal/acts_as_indexed) and
40
+ unpack it into the <tt>vendor/plugins</tt> directory of your rails app.
41
+
42
+ === Upgrade
43
+
44
+ When upgrading to a new version of acts_as_indexed it is recommended you
45
+ delete the index directory and allow it to be rebuilt.
41
46
 
42
47
 
43
48
  == Usage
@@ -97,7 +102,7 @@ of any matching records.
97
102
 
98
103
  # Pass any of the ActiveRecord find options to the search.
99
104
  my_search_results = Post.find_with_index('my search query',{:limit => 10}) # return the first 10 matches.
100
-
105
+
101
106
  # Returns array of IDs ordered by relevance.
102
107
  my_search_results = Post.find_with_index('my search query',{},{:ids_only => true}) # => [12,19,33...
103
108
 
@@ -128,7 +133,7 @@ The following query operators are supported:
128
133
  ==== With Relevance
129
134
 
130
135
  Pagination is supported via the +paginate_search+ method whose first argument is the search query, followed all the standard will_paginate arguments.
131
-
136
+
132
137
  @images = Image.paginate_search('girl', :page => 1, :per_page => 5)
133
138
 
134
139
  ==== Without Relevance (Scope)
@@ -141,12 +146,11 @@ fashion.
141
146
  === Further Configuration
142
147
 
143
148
  A config block can be provided in your environment files or initializers.
144
- Example showing defaults:
149
+ Example showing changing the min word size:
145
150
 
146
151
  ActsAsIndexed.configure do |config|
147
- config.index_file = [Rails.root.to_s,'index']
148
- config.index_file_depth = 3
149
152
  config.min_word_size = 3
153
+ # More config as required...
150
154
  end
151
155
 
152
156
  A full rundown of the available configuration options can be found in
@@ -188,5 +192,5 @@ Future releases will be looking to add the following features:
188
192
  * Optional html scrubbing during indexing.
189
193
  * Ranking affected by field weightings.
190
194
  * Support for DataMapper, Sequel and the various MongoDB ORMs.
191
- * UTF-8 support. See the current solution in the following Gist:
195
+ * UTF-8 support. See the current solution in the following Gist:
192
196
  https://gist.github.com/193903bb4e0d6e5debe1
data/Rakefile CHANGED
@@ -47,5 +47,5 @@ begin
47
47
  end
48
48
  Jeweler::GemcutterTasks.new
49
49
  rescue LoadError
50
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
50
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
51
51
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.7
1
+ 0.7.0
@@ -1,60 +1,61 @@
1
1
  # Generated by jeweler
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{acts_as_indexed}
8
- s.version = "0.6.7"
8
+ s.version = "0.7.0"
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{2011-02-07}
12
+ s.date = %q{2011-02-11}
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 = [
16
16
  "README.rdoc"
17
17
  ]
18
18
  s.files = [
19
- ".gitignore",
20
- "CHANGELOG",
21
- "Gemfile",
22
- "Gemfile.lock",
23
- "MIT-LICENSE",
24
- "README.rdoc",
25
- "Rakefile",
26
- "VERSION",
27
- "acts_as_indexed.gemspec",
28
- "lib/acts_as_indexed.rb",
29
- "lib/acts_as_indexed/configuration.rb",
30
- "lib/acts_as_indexed/search_atom.rb",
31
- "lib/acts_as_indexed/search_index.rb",
32
- "lib/acts_as_indexed/storage.rb",
33
- "lib/will_paginate_search.rb",
34
- "rails/init.rb",
35
- "test/abstract_unit.rb",
36
- "test/acts_as_indexed_test.rb",
37
- "test/configuration_test.rb",
38
- "test/database.yml",
39
- "test/fixtures/post.rb",
40
- "test/fixtures/posts.yml",
41
- "test/schema.rb",
42
- "test/search_atom_test.rb",
43
- "test/search_index_test.rb"
19
+ "CHANGELOG",
20
+ "Gemfile",
21
+ "Gemfile.lock",
22
+ "MIT-LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "acts_as_indexed.gemspec",
27
+ "lib/acts_as_indexed.rb",
28
+ "lib/acts_as_indexed/class_methods.rb",
29
+ "lib/acts_as_indexed/configuration.rb",
30
+ "lib/acts_as_indexed/instance_methods.rb",
31
+ "lib/acts_as_indexed/search_atom.rb",
32
+ "lib/acts_as_indexed/search_index.rb",
33
+ "lib/acts_as_indexed/singleton_methods.rb",
34
+ "lib/acts_as_indexed/storage.rb",
35
+ "lib/will_paginate_search.rb",
36
+ "rails/init.rb",
37
+ "test/abstract_unit.rb",
38
+ "test/acts_as_indexed_test.rb",
39
+ "test/configuration_test.rb",
40
+ "test/database.yml",
41
+ "test/fixtures/post.rb",
42
+ "test/fixtures/posts.yml",
43
+ "test/schema.rb",
44
+ "test/search_atom_test.rb",
45
+ "test/search_index_test.rb"
44
46
  ]
45
47
  s.homepage = %q{http://github.com/dougal/acts_as_indexed}
46
- s.rdoc_options = ["--charset=UTF-8"]
47
48
  s.require_paths = ["lib"]
48
49
  s.rubygems_version = %q{1.3.7}
49
50
  s.summary = %q{Acts As Indexed is a plugin which provides a pain-free way to add fulltext search to your Ruby on Rails app}
50
51
  s.test_files = [
51
52
  "test/abstract_unit.rb",
52
- "test/acts_as_indexed_test.rb",
53
- "test/configuration_test.rb",
54
- "test/fixtures/post.rb",
55
- "test/schema.rb",
56
- "test/search_atom_test.rb",
57
- "test/search_index_test.rb"
53
+ "test/acts_as_indexed_test.rb",
54
+ "test/configuration_test.rb",
55
+ "test/fixtures/post.rb",
56
+ "test/schema.rb",
57
+ "test/search_atom_test.rb",
58
+ "test/search_index_test.rb"
58
59
  ]
59
60
 
60
61
  if s.respond_to? :specification_version then
@@ -1,10 +1,13 @@
1
1
  # ActsAsIndexed
2
- # Copyright (c) 2007 - 2010 Douglas F Shearer.
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
3
  # http://douglasfshearer.com
4
4
  # Distributed under the MIT license as included with this plugin.
5
5
 
6
6
  require 'active_record'
7
7
 
8
+ require 'acts_as_indexed/class_methods'
9
+ require 'acts_as_indexed/instance_methods'
10
+ require 'acts_as_indexed/singleton_methods'
8
11
  require 'acts_as_indexed/configuration'
9
12
  require 'acts_as_indexed/search_index'
10
13
  require 'acts_as_indexed/search_atom'
@@ -12,6 +15,10 @@ require 'acts_as_indexed/storage'
12
15
 
13
16
  module ActsAsIndexed #:nodoc:
14
17
 
18
+ # This is the last version of the plugin where the index structure was
19
+ # changed in some manner. Is only changed when necessary, not every release.
20
+ INDEX_VERSION = '0.6.8'
21
+
15
22
  # Holds the default configuration for acts_as_indexed.
16
23
 
17
24
  @configuration = Configuration.new
@@ -40,190 +47,6 @@ module ActsAsIndexed #:nodoc:
40
47
  mod.extend(ClassMethods)
41
48
  end
42
49
 
43
- module ClassMethods
44
-
45
- # Declares a class as searchable.
46
- #
47
- # ====options:
48
- # fields:: Names of fields to include in the index. Symbols pointing to
49
- # instance methods of your model may also be given here.
50
- # index_file_depth:: Tuning value for the index partitioning. Larger
51
- # values result in quicker searches, but slower
52
- # indexing. Default is 3.
53
- # min_word_size:: Sets the minimum length for a word in a query. Words
54
- # shorter than this value are ignored in searches
55
- # unless preceded by the '+' operator. Default is 3.
56
- # index_file:: Sets the location for the index. By default this is
57
- # RAILS_ROOT/index. Specify as an array. Heroku, for
58
- # example would use RAILS_ROOT/tmp/index, which would be
59
- # set as [Rails.root,'tmp','index]
60
-
61
- def acts_as_indexed(options = {})
62
- class_eval do
63
- extend ActsAsIndexed::SingletonMethods
64
- end
65
- include ActsAsIndexed::InstanceMethods
66
-
67
- after_create :add_to_index
68
- before_update :update_index
69
- after_destroy :remove_from_index
70
-
71
- # scope for Rails 3.x, named_scope for Rails 2.x.
72
- if self.respond_to?(:where)
73
- scope :with_query, lambda { |query| where("#{table_name}.id IN (?)", search_index(query, {}, {:ids_only => true})) }
74
- else
75
- named_scope :with_query, lambda { |query| { :conditions => ["#{table_name}.id IN (?)", search_index(query, {}, {:ids_only => true}) ] } }
76
- end
77
-
78
- cattr_accessor :aai_config, :aai_fields
79
-
80
- self.aai_fields = options.delete(:fields)
81
- raise(ArgumentError, 'no fields specified') if self.aai_fields.nil? || self.aai_fields.empty?
82
-
83
- self.aai_config = ActsAsIndexed.configuration.dup
84
- self.aai_config.if_proc = options.delete(:if)
85
- options.each do |k, v|
86
- self.aai_config.send("#{k}=", v)
87
- end
88
-
89
- # Add the Rails environment and this model's name to the index file path.
90
- self.aai_config.index_file = self.aai_config.index_file.join(Rails.env, self.name)
91
- end
92
-
93
- # Adds the passed +record+ to the index. Index is built if it does not already exist. Clears the query cache.
94
-
95
- def index_add(record)
96
- build_index unless aai_config.index_file.directory?
97
- index = SearchIndex.new(aai_config.index_file, aai_config.index_file_depth, aai_fields, aai_config.min_word_size, aai_config.if_proc)
98
- index.add_record(record)
99
- @query_cache = {}
100
- end
101
-
102
- # Removes the passed +record+ from the index. Clears the query cache.
103
-
104
- def index_remove(record)
105
- index = SearchIndex.new(aai_config.index_file, aai_config.index_file_depth, aai_fields, aai_config.min_word_size, aai_config.if_proc)
106
- index.remove_record(record)
107
- @query_cache = {}
108
- end
109
-
110
- # Updates the index.
111
- # 1. Removes the previous version of the record from the index
112
- # 2. Adds the new version to the index.
113
-
114
- def index_update(record)
115
- build_index unless aai_config.index_file.directory?
116
- index = SearchIndex.new(aai_config.index_file, aai_config.index_file_depth, aai_fields, aai_config.min_word_size, aai_config.if_proc)
117
- index.update_record(record,find(record.id))
118
- @query_cache = {}
119
- end
120
-
121
- # Finds instances matching the terms passed in +query+. Terms are ANDed by
122
- # default. Returns an array of model instances or, if +ids_only+ is
123
- # true, an array of integer IDs.
124
- #
125
- # Keeps a cache of matched IDs for the current session to speed up
126
- # multiple identical searches.
127
- #
128
- # ====find_options
129
- # Same as ActiveRecord#find options hash. An :order key will override
130
- # the relevance ranking
131
- #
132
- # ====options
133
- # ids_only:: Method returns an array of integer IDs when set to true.
134
- # no_query_cache:: Turns off the query cache when set to true. Useful for testing.
135
-
136
- def search_index(query, find_options={}, options={})
137
- # Clear the query cache off if the key is set.
138
- @query_cache = {} if (options.has_key?('no_query_cache') || options[:no_query_cache])
139
- if !@query_cache || !@query_cache[query]
140
- logger.debug('Query not in cache, running search.')
141
- build_index unless aai_config.index_file.directory?
142
- index = SearchIndex.new(aai_config.index_file, aai_config.index_file_depth, aai_fields, aai_config.min_word_size, aai_config.if_proc)
143
- (@query_cache ||= {})[query] = index.search(query)
144
- else
145
- logger.debug('Query held in cache.')
146
- end
147
- return @query_cache[query].sort.reverse.map{|r| r.first} if options[:ids_only] || @query_cache[query].empty?
148
-
149
- # slice up the results by offset and limit
150
- offset = find_options[:offset] || 0
151
- limit = find_options.include?(:limit) ? find_options[:limit] : @query_cache[query].size
152
- part_query = @query_cache[query].sort.reverse.slice(offset,limit).map{|r| r.first}
153
-
154
- # Set these to nil as we are dealing with the pagination by setting
155
- # exactly what records we want.
156
- find_options[:offset] = nil
157
- find_options[:limit] = nil
158
-
159
- with_scope :find => find_options do
160
- # Doing the find like this eliminates the possibility of errors occuring
161
- # on either missing records (out-of-sync) or an empty results array.
162
- records = find(:all, :conditions => [ "#{table_name}.id IN (?)", part_query])
163
-
164
- if find_options.include?(:order)
165
- records # Just return the records without ranking them.
166
- else
167
- # Results come back in random order from SQL, so order again.
168
- ranked_records = {}
169
- records.each do |r|
170
- ranked_records[r] = @query_cache[query][r.id]
171
- end
172
-
173
- ranked_records.to_a.sort_by{|a| a.last }.reverse.map{|r| r.first}
174
- end
175
- end
176
-
177
- end
178
-
179
- private
180
-
181
- # Builds an index from scratch for the current model class.
182
- def build_index
183
- index = SearchIndex.new(aai_config.index_file, aai_config.index_file_depth, aai_fields, aai_config.min_word_size, aai_config.if_proc)
184
- find_in_batches({ :batch_size => 500 }) do |records|
185
- index.add_records(records)
186
- end
187
- end
188
-
189
- end
190
-
191
- # Adds model class singleton methods.
192
- module SingletonMethods
193
-
194
- # Finds instances matching the terms passed in +query+.
195
- #
196
- # See ActsAsIndexed::ClassMethods#search_index.
197
- def find_with_index(query='', find_options = {}, options = {})
198
- search_index(query, find_options, options)
199
- end
200
-
201
- end
202
-
203
- # Adds model class instance methods.
204
- # Methods are called automatically by ActiveRecord on +save+, +destroy+,
205
- # and +update+ of model instances.
206
- module InstanceMethods
207
-
208
- # Adds the current model instance to index.
209
- # Called by ActiveRecord on +save+.
210
- def add_to_index
211
- self.class.index_add(self)
212
- end
213
-
214
- # Removes the current model instance to index.
215
- # Called by ActiveRecord on +destroy+.
216
- def remove_from_index
217
- self.class.index_remove(self)
218
- end
219
-
220
- # Updates current model instance index.
221
- # Called by ActiveRecord on +update+.
222
- def update_index
223
- self.class.index_update(self)
224
- end
225
- end
226
-
227
50
  end
228
51
 
229
52
  # reopen ActiveRecord and include all the above to make
@@ -0,0 +1,160 @@
1
+ # ActsAsIndexed
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
+ # http://douglasfshearer.com
4
+ # Distributed under the MIT license as included with this plugin.
5
+
6
+ module ActsAsIndexed
7
+
8
+ module ClassMethods
9
+
10
+ # Declares a class as searchable.
11
+ #
12
+ # ====options:
13
+ # fields:: Names of fields to include in the index. Symbols pointing to
14
+ # instance methods of your model may also be given here.
15
+ # index_file_depth:: Tuning value for the index partitioning. Larger
16
+ # values result in quicker searches, but slower
17
+ # indexing. Default is 3.
18
+ # min_word_size:: Sets the minimum length for a word in a query. Words
19
+ # shorter than this value are ignored in searches
20
+ # unless preceded by the '+' operator. Default is 3.
21
+ # index_file:: Sets the location for the index. By default this is
22
+ # RAILS_ROOT/index. Specify as an array. Heroku, for
23
+ # example would use RAILS_ROOT/tmp/index, which would be
24
+ # set as [Rails.root,'tmp','index]
25
+
26
+ def acts_as_indexed(options = {})
27
+ class_eval do
28
+ extend ActsAsIndexed::SingletonMethods
29
+ end
30
+ include ActsAsIndexed::InstanceMethods
31
+
32
+ after_create :add_to_index
33
+ before_update :update_index
34
+ after_destroy :remove_from_index
35
+
36
+ # scope for Rails 3.x, named_scope for Rails 2.x.
37
+ if self.respond_to?(:where)
38
+ scope :with_query, lambda { |query| where("#{table_name}.id IN (?)", search_index(query, {}, {:ids_only => true})) }
39
+ else
40
+ named_scope :with_query, lambda { |query| { :conditions => ["#{table_name}.id IN (?)", search_index(query, {}, {:ids_only => true}) ] } }
41
+ end
42
+
43
+ cattr_accessor :aai_config, :aai_fields
44
+
45
+ self.aai_fields = options.delete(:fields)
46
+ raise(ArgumentError, 'no fields specified') if self.aai_fields.nil? || self.aai_fields.empty?
47
+
48
+ self.aai_config = ActsAsIndexed.configuration.dup
49
+ self.aai_config.if_proc = options.delete(:if)
50
+ options.each do |k, v|
51
+ self.aai_config.send("#{k}=", v)
52
+ end
53
+
54
+ # Add the Rails environment and this model's name to the index file path.
55
+ self.aai_config.index_file = self.aai_config.index_file.join(Rails.env, self.name)
56
+ end
57
+
58
+ # Adds the passed +record+ to the index. Index is built if it does not already exist. Clears the query cache.
59
+
60
+ def index_add(record)
61
+ build_index unless aai_config.index_file.directory?
62
+ index = new_index
63
+ index.add_record(record)
64
+ @query_cache = {}
65
+ end
66
+
67
+ # Removes the passed +record+ from the index. Clears the query cache.
68
+
69
+ def index_remove(record)
70
+ index = new_index
71
+ index.remove_record(record)
72
+ @query_cache = {}
73
+ end
74
+
75
+ # Updates the index.
76
+ # 1. Removes the previous version of the record from the index
77
+ # 2. Adds the new version to the index.
78
+
79
+ def index_update(record)
80
+ build_index unless aai_config.index_file.directory?
81
+ index = new_index
82
+ index.update_record(record,find(record.id))
83
+ @query_cache = {}
84
+ end
85
+
86
+ # Finds instances matching the terms passed in +query+. Terms are ANDed by
87
+ # default. Returns an array of model instances or, if +ids_only+ is
88
+ # true, an array of integer IDs.
89
+ #
90
+ # Keeps a cache of matched IDs for the current session to speed up
91
+ # multiple identical searches.
92
+ #
93
+ # ====find_options
94
+ # Same as ActiveRecord#find options hash. An :order key will override
95
+ # the relevance ranking
96
+ #
97
+ # ====options
98
+ # ids_only:: Method returns an array of integer IDs when set to true.
99
+ # no_query_cache:: Turns off the query cache when set to true. Useful for testing.
100
+
101
+ def search_index(query, find_options={}, options={})
102
+ # Clear the query cache off if the key is set.
103
+ @query_cache = {} if (options.has_key?('no_query_cache') || options[:no_query_cache])
104
+ if !@query_cache || !@query_cache[query]
105
+ logger.debug('Query not in cache, running search.')
106
+ build_index unless aai_config.index_file.directory?
107
+ index = new_index
108
+ (@query_cache ||= {})[query] = index.search(query)
109
+ else
110
+ logger.debug('Query held in cache.')
111
+ end
112
+ return @query_cache[query].sort.reverse.map{|r| r.first} if options[:ids_only] || @query_cache[query].empty?
113
+
114
+ # slice up the results by offset and limit
115
+ offset = find_options[:offset] || 0
116
+ limit = find_options.include?(:limit) ? find_options[:limit] : @query_cache[query].size
117
+ part_query = @query_cache[query].sort.reverse.slice(offset,limit).map{|r| r.first}
118
+
119
+ # Set these to nil as we are dealing with the pagination by setting
120
+ # exactly what records we want.
121
+ find_options[:offset] = nil
122
+ find_options[:limit] = nil
123
+
124
+ with_scope :find => find_options do
125
+ # Doing the find like this eliminates the possibility of errors occuring
126
+ # on either missing records (out-of-sync) or an empty results array.
127
+ records = find(:all, :conditions => [ "#{table_name}.id IN (?)", part_query])
128
+
129
+ if find_options.include?(:order)
130
+ records # Just return the records without ranking them.
131
+ else
132
+ # Results come back in random order from SQL, so order again.
133
+ ranked_records = {}
134
+ records.each do |r|
135
+ ranked_records[r] = @query_cache[query][r.id]
136
+ end
137
+
138
+ ranked_records.to_a.sort_by{|a| a.last }.reverse.map{|r| r.first}
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+ private
145
+
146
+ def new_index
147
+ SearchIndex.new(aai_fields, aai_config)
148
+ end
149
+
150
+ # Builds an index from scratch for the current model class.
151
+ def build_index
152
+ index = new_index
153
+ find_in_batches({ :batch_size => 500 }) do |records|
154
+ index.add_records(records)
155
+ end
156
+ end
157
+
158
+ end
159
+
160
+ end
@@ -1,5 +1,5 @@
1
1
  # ActsAsIndexed
2
- # Copyright (c) 2007 - 2010 Douglas F Shearer.
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
3
  # http://douglasfshearer.com
4
4
  # Distributed under the MIT license as included with this plugin.
5
5
 
@@ -22,24 +22,27 @@ module ActsAsIndexed
22
22
  attr_reader :min_word_size
23
23
 
24
24
  # Proc that allows you to turn on or off index for a record.
25
- # Useful if you don't want the index to be updated if the target model is
26
- # should not return up in results, such as a draft post.
25
+ # Useful if you don't want an object to be placed in the index, such as a
26
+ # draft post.
27
27
  attr_accessor :if_proc
28
28
 
29
+ # Enable or disable case sensitivity.
30
+ # Set to true to enable.
31
+ # Default is false.
32
+ attr_accessor :case_sensitive
33
+
29
34
  def initialize
30
- @index_file = nil
35
+ @index_file = nil
31
36
  @index_file_depth = 3
32
- @min_word_size = 3
33
- @if_proc = if_proc
37
+ @min_word_size = 3
38
+ @if_proc = if_proc
39
+ @case_sensitive = false
34
40
  end
35
41
 
36
42
  # Since we cannot expect Rails to be available on load, it is best to put
37
43
  # off setting the index_file attribute until as late as possible.
38
44
  def index_file
39
- if @index_file.nil?
40
- @index_file = default_index_file
41
- end
42
- @index_file
45
+ @index_file ||= default_index_file
43
46
  end
44
47
 
45
48
  def index_file=(file_path)
@@ -67,7 +70,7 @@ module ActsAsIndexed
67
70
  end
68
71
 
69
72
  private
70
-
73
+
71
74
  def default_index_file
72
75
  if Rails.root.writable?
73
76
  Rails.root.join('index')
@@ -0,0 +1,32 @@
1
+ # ActsAsIndexed
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
+ # http://douglasfshearer.com
4
+ # Distributed under the MIT license as included with this plugin.
5
+
6
+ module ActsAsIndexed
7
+
8
+ # Adds model class instance methods.
9
+ # Methods are called automatically by ActiveRecord on +save+, +destroy+,
10
+ # and +update+ of model instances.
11
+ module InstanceMethods
12
+
13
+ # Adds the current model instance to index.
14
+ # Called by ActiveRecord on +save+.
15
+ def add_to_index
16
+ self.class.index_add(self)
17
+ end
18
+
19
+ # Removes the current model instance to index.
20
+ # Called by ActiveRecord on +destroy+.
21
+ def remove_from_index
22
+ self.class.index_remove(self)
23
+ end
24
+
25
+ # Updates current model instance index.
26
+ # Called by ActiveRecord on +update+.
27
+ def update_index
28
+ self.class.index_update(self)
29
+ end
30
+ end
31
+
32
+ end
@@ -1,5 +1,5 @@
1
1
  # ActsAsIndexed
2
- # Copyright (c) 2007 - 2010 Douglas F Shearer.
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
3
  # http://douglasfshearer.com
4
4
  # Distributed under the MIT license as included with this plugin.
5
5
 
@@ -1,23 +1,21 @@
1
1
  # ActsAsIndexed
2
- # Copyright (c) 2007 - 2010 Douglas F Shearer.
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
3
  # http://douglasfshearer.com
4
4
  # Distributed under the MIT license as included with this plugin.
5
5
 
6
6
  module ActsAsIndexed #:nodoc:
7
7
  class SearchIndex
8
8
 
9
- # root:: Location of index on filesystem as a Pathname.
10
- # index_depth:: Degree of index partitioning.
11
9
  # fields:: Fields or instance methods of ActiveRecord model to be indexed.
12
- # min_word_size:: Smallest query term that will be run through search.
13
- # if_proc:: A Proc. If the proc is true, the index gets added, if false if doesn't
14
- def initialize(root, index_depth, fields, min_word_size, if_proc=Proc.new{true})
15
- @storage = Storage.new(Pathname.new(root.to_s), index_depth)
10
+ # config:: ActsAsIndexed::Configuration instance.
11
+ def initialize(fields, config)
12
+ @storage = Storage.new(Pathname.new(config.index_file.to_s), config.index_file_depth)
16
13
  @fields = fields
17
14
  @atoms = {}
18
- @min_word_size = min_word_size
15
+ @min_word_size = config.min_word_size
19
16
  @records_size = @storage.record_count
20
- @if_proc = if_proc
17
+ @case_sensitive = config.case_sensitive
18
+ @if_proc = config.if_proc
21
19
  end
22
20
 
23
21
  # Adds +record+ to the index.
@@ -25,23 +23,30 @@ module ActsAsIndexed #:nodoc:
25
23
  return unless @if_proc.call(record)
26
24
 
27
25
  condensed_record = condense_record(record)
28
- atoms = add_occurences(condensed_record,record.id)
29
-
26
+ atoms = add_occurences(condensed_record, record.id)
27
+
30
28
  @storage.add(atoms)
31
29
  end
32
30
 
33
31
  # Adds multiple records to the index. Accepts an array of +records+.
34
32
  def add_records(records)
33
+ atoms = {}
34
+
35
35
  records.each do |record|
36
- add_record(record)
36
+ next unless @if_proc.call(record)
37
+
38
+ condensed_record = condense_record(record)
39
+ atoms = add_occurences(condensed_record, record.id, atoms)
37
40
  end
41
+
42
+ @storage.add(atoms)
38
43
  end
39
44
 
40
45
  # Removes +record+ from the index.
41
46
  def remove_record(record)
42
47
  condensed_record = condense_record(record)
43
48
  atoms = add_occurences(condensed_record,record.id)
44
-
49
+
45
50
  @storage.remove(atoms)
46
51
  end
47
52
 
@@ -104,8 +109,7 @@ module ActsAsIndexed #:nodoc:
104
109
  r1.merge(r2) { |r_id,old_val,new_val| old_val + new_val}
105
110
  end
106
111
 
107
- def add_occurences(condensed_record,record_id)
108
- atoms = {}
112
+ def add_occurences(condensed_record, record_id, atoms={})
109
113
  condensed_record.each_with_index do |atom_name, i|
110
114
  atoms[atom_name] = SearchAtom.new unless atoms.include?(atom_name)
111
115
  atoms[atom_name].add_position(record_id, i)
@@ -248,7 +252,8 @@ module ActsAsIndexed #:nodoc:
248
252
 
249
253
 
250
254
  def cleanup_atoms(s, limit_size=false, min_size = @min_word_size || 3)
251
- atoms = s.downcase.gsub(/\W/,' ').squeeze(' ').split
255
+ s = @case_sensitive ? s : s.downcase
256
+ atoms = s.gsub(/\W/,' ').squeeze(' ').split
252
257
  return atoms unless limit_size
253
258
  atoms.reject{|w| w.size < min_size}
254
259
  end
@@ -0,0 +1,20 @@
1
+ # ActsAsIndexed
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
+ # http://douglasfshearer.com
4
+ # Distributed under the MIT license as included with this plugin.
5
+
6
+ module ActsAsIndexed
7
+
8
+ # Adds model class singleton methods.
9
+ module SingletonMethods
10
+
11
+ # Finds instances matching the terms passed in +query+.
12
+ #
13
+ # See ActsAsIndexed::ClassMethods#search_index.
14
+ def find_with_index(query='', find_options = {}, options = {})
15
+ search_index(query, find_options, options)
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -1,13 +1,19 @@
1
1
  # ActsAsIndexed
2
- # Copyright (c) 2007 - 2010 Douglas F Shearer.
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
3
  # http://douglasfshearer.com
4
4
  # Distributed under the MIT license as included with this plugin.
5
5
 
6
6
  module ActsAsIndexed #:nodoc:
7
7
  class Storage
8
8
 
9
+ class OldIndexVersion < Exception;end
10
+
11
+ INDEX_FILE_EXTENSION = '.ind'
12
+ TEMP_FILE_EXTENSION = '.tmp'
13
+
9
14
  def initialize(path, prefix_size)
10
15
  @path = path
16
+ @size_path = path.join('size')
11
17
  @prefix_size = prefix_size
12
18
  prepare
13
19
  end
@@ -17,7 +23,6 @@ module ActsAsIndexed #:nodoc:
17
23
  operate(:+, atoms)
18
24
 
19
25
  update_record_count(1)
20
-
21
26
  end
22
27
 
23
28
  # Takes a hash of atoms and removes these from storage.
@@ -35,6 +40,7 @@ module ActsAsIndexed #:nodoc:
35
40
  atom_names.uniq.collect{|a| encoded_prefix(a) }.uniq.each do |prefix|
36
41
  pattern = @path.join(prefix.to_s).to_s
37
42
  pattern += '*' if start
43
+ pattern += INDEX_FILE_EXTENSION
38
44
 
39
45
  Pathname.glob(pattern).each do |atom_file|
40
46
  atom_file.open do |f|
@@ -48,13 +54,7 @@ module ActsAsIndexed #:nodoc:
48
54
 
49
55
  # Returns the number of records currently stored in this index.
50
56
  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
57
+ @size_path.read.to_i
58
58
 
59
59
  # This is a bit horrible.
60
60
  rescue Errno::ENOENT
@@ -78,35 +78,56 @@ module ActsAsIndexed #:nodoc:
78
78
  end
79
79
 
80
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)
81
+ path = @path.join(e_p.to_s + INDEX_FILE_EXTENSION)
82
+
83
+ lock_file(path) do
84
+
85
+ if path.exist?
86
+ from_file = path.open do |f|
87
+ Marshal.load(f)
88
+ end
89
+ else
90
+ from_file = {}
86
91
  end
87
- else
88
- from_file = {}
89
- end
90
92
 
91
- atoms = from_file.merge(atoms){ |k,o,n| o.send(operation, n) }
93
+ atoms = from_file.merge(atoms){ |k,o,n| o.send(operation, n) }
92
94
 
93
- path.open("w+") do |f|
94
- Marshal.dump(atoms,f)
95
- end
95
+ write_file(path) do |f|
96
+ Marshal.dump(atoms,f)
97
+ end
98
+ end # end lock.
99
+
96
100
  end
97
101
  end
98
102
 
99
103
  def update_record_count(delta)
100
- new_count = self.record_count + delta
101
- new_count = 0 if new_count < 0
104
+ lock_file(@size_path) do
105
+ new_count = self.record_count + delta
106
+ new_count = 0 if new_count < 0
102
107
 
103
- @path.join('size').open('w+') do |f|
104
- Marshal.dump(new_count,f)
108
+ write_file(@size_path) do |f|
109
+ f.write(new_count)
110
+ end
105
111
  end
106
112
  end
107
113
 
108
114
  def prepare
109
- @path.mkpath unless @path.exist?
115
+ version_path = @path.join('version')
116
+
117
+ if @path.exist?
118
+ unless version_path.exist? && version_path.read == ActsAsIndexed::INDEX_VERSION
119
+ raise OldIndexVersion, "Index was created prior to version #{ActsAsIndexed::INDEX_VERSION}. Please delete it, it will be rebuilt automatically."
120
+ end
121
+
122
+ else
123
+ @path.mkpath
124
+
125
+ # Do we need to lock for this? I don't think so as it is only ever making
126
+ # a creation, not a modification.
127
+ write_file(version_path) do |f|
128
+ f.write(ActsAsIndexed::INDEX_VERSION)
129
+ end
130
+ end
110
131
  end
111
132
 
112
133
  def encoded_prefix(atom)
@@ -132,5 +153,33 @@ module ActsAsIndexed #:nodoc:
132
153
  end
133
154
  end
134
155
 
156
+ def write_file(file_path)
157
+ new_file = file_path.to_s
158
+ tmp_file = new_file + TEMP_FILE_EXTENSION
159
+
160
+ File.open(tmp_file, 'w+') do |f|
161
+ yield(f)
162
+ end
163
+
164
+ FileUtils.mv(tmp_file, new_file)
165
+ end
166
+
167
+ # Borrowed from Rails' ActiveSupport FileStore. Also under MIT licence.
168
+ # Lock a file for a block so only one process can modify it at a time.
169
+ def lock_file(file_path, &block) # :nodoc:
170
+ if file_path.exist?
171
+ file_path.open('r') do |f|
172
+ begin
173
+ f.flock File::LOCK_EX
174
+ yield
175
+ ensure
176
+ f.flock File::LOCK_UN
177
+ end
178
+ end
179
+ else
180
+ yield
181
+ end
182
+ end
183
+
135
184
  end
136
185
  end
@@ -1,5 +1,5 @@
1
1
  # WillPaginateSearch
2
- # Copyright (c) 2007 - 2010 Douglas F Shearer.
2
+ # Copyright (c) 2007 - 2011 Douglas F Shearer.
3
3
  # http://douglasfshearer.com
4
4
 
5
5
  module ActsAsIndexed
@@ -196,6 +196,17 @@ class ActsAsIndexedTest < ActiveSupport::TestCase
196
196
  assert_equal 1, Post.find_with_index('crane',{},{ :no_query_cache => true, :ids_only => true}).size
197
197
  end
198
198
 
199
+ def test_case_insensitive
200
+ Post.acts_as_indexed :fields => [:title, :body], :case_sensitive => true
201
+ destroy_index
202
+
203
+ assert_equal 1, Post.find_with_index('Ellis', {}, { :no_query_cache => true, :ids_only => true}).size
204
+ assert_equal 0, Post.find_with_index('ellis', {}, { :no_query_cache => true, :ids_only => true}).size
205
+
206
+ assert_equal 3, Post.find_with_index('The', {}, { :no_query_cache => true, :ids_only => true}).size
207
+ assert_equal 5, Post.find_with_index('the', {}, { :no_query_cache => true, :ids_only => true}).size
208
+ end
209
+
199
210
  private
200
211
 
201
212
  def result_ids(query)
@@ -12,8 +12,8 @@ class SearchIndexTest < ActiveSupport::TestCase
12
12
 
13
13
  private
14
14
 
15
- def build_search_index(root = index_loc, index_depth = 2, fields = [:title, :body], min_word_size = 3)
16
- SearchIndex.new([root], index_depth, fields, min_word_size)
15
+ def build_search_index(fields, config)
16
+ SearchIndex.new(fields, config)
17
17
  end
18
18
 
19
19
  end
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: 9
5
- prerelease: false
4
+ hash: 3
5
+ prerelease:
6
6
  segments:
7
7
  - 0
8
- - 6
9
8
  - 7
10
- version: 0.6.7
9
+ - 0
10
+ version: 0.7.0
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: 2011-02-07 00:00:00 +00:00
18
+ date: 2011-02-11 00:00:00 +00:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -28,7 +28,6 @@ extensions: []
28
28
  extra_rdoc_files:
29
29
  - README.rdoc
30
30
  files:
31
- - .gitignore
32
31
  - CHANGELOG
33
32
  - Gemfile
34
33
  - Gemfile.lock
@@ -38,9 +37,12 @@ files:
38
37
  - VERSION
39
38
  - acts_as_indexed.gemspec
40
39
  - lib/acts_as_indexed.rb
40
+ - lib/acts_as_indexed/class_methods.rb
41
41
  - lib/acts_as_indexed/configuration.rb
42
+ - lib/acts_as_indexed/instance_methods.rb
42
43
  - lib/acts_as_indexed/search_atom.rb
43
44
  - lib/acts_as_indexed/search_index.rb
45
+ - lib/acts_as_indexed/singleton_methods.rb
44
46
  - lib/acts_as_indexed/storage.rb
45
47
  - lib/will_paginate_search.rb
46
48
  - rails/init.rb
@@ -58,8 +60,8 @@ homepage: http://github.com/dougal/acts_as_indexed
58
60
  licenses: []
59
61
 
60
62
  post_install_message:
61
- rdoc_options:
62
- - --charset=UTF-8
63
+ rdoc_options: []
64
+
63
65
  require_paths:
64
66
  - lib
65
67
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -83,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
85
  requirements: []
84
86
 
85
87
  rubyforge_project:
86
- rubygems_version: 1.3.7
88
+ rubygems_version: 1.5.2
87
89
  signing_key:
88
90
  specification_version: 3
89
91
  summary: Acts As Indexed is a plugin which provides a pain-free way to add fulltext search to your Ruby on Rails app
data/.gitignore DELETED
@@ -1,6 +0,0 @@
1
- rdoc
2
- test/test.log
3
- coverage
4
- index
5
- pkg
6
- .bundle