acts_as_indexed 0.6.7 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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