acts_as_ferret 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -22,6 +22,9 @@ project's config/environment.rb:
22
22
 
23
23
  <tt>require 'acts_as_ferret'</tt>
24
24
 
25
+ Call the aaf_install script inside your project directory to install the sample
26
+ config file and the drb server start/stop script.
27
+
25
28
 
26
29
  == Usage
27
30
 
data/bin/aaf_install ADDED
@@ -0,0 +1,23 @@
1
+ # acts_as_ferret gem install script
2
+ # Use inside the root of your Rails project
3
+ require 'fileutils'
4
+
5
+ @basedir = File.join(File.dirname(__FILE__), '..')
6
+
7
+ def install(dir, file, executable=false)
8
+ puts "Installing: #{file}"
9
+ target = File.join('.', dir, file)
10
+ if File.exists?(target)
11
+ puts "#{target} already exists, skipping"
12
+ else
13
+ FileUtils.cp File.join(@basedir, dir, file), target
14
+ FileUtils.chmod 0755, target if executable
15
+ end
16
+ end
17
+
18
+
19
+ install 'script', 'ferret_server', true
20
+ install 'config', 'ferret_server.yml'
21
+
22
+ puts IO.read(File.join(@basedir, 'README'))
23
+
@@ -1,12 +1,23 @@
1
+ # configuration for the acts_as_ferret DRb server
2
+ # host: where to reach the DRb server (used by application processes to contact the server)
3
+ # port: which port the server should listen on
4
+ # pid_file: location of the server's pid file (relative to RAILS_ROOT)
5
+ # log_file: log file (default: RAILS_ROOT/log/ferret_server.log
6
+ # log_level: log level for the server's logger
1
7
  production:
2
- host: ferret.yourdomain.com
3
- port: 9010
4
- pid_file: log/ferret.pid
5
- development:
6
8
  host: localhost
7
9
  port: 9010
8
10
  pid_file: log/ferret.pid
9
- test:
10
- host: localhost
11
- port: 9009
12
- pid_file: log/ferret.pid
11
+ log_file: log/ferret_server.log
12
+ log_level: warn
13
+
14
+ # aaf won't try to use the DRb server in environments that are not
15
+ # configured here.
16
+ #development:
17
+ # host: localhost
18
+ # port: 9010
19
+ # pid_file: log/ferret.pid
20
+ #test:
21
+ # host: localhost
22
+ # port: 9009
23
+ # pid_file: log/ferret.pid
data/doc/README.win32 ADDED
@@ -0,0 +1,23 @@
1
+ Credits
2
+ =======
3
+
4
+ The Win32 service support scripts have been written by
5
+ Herryanto Siatono <herryanto@pluitsolutions.com>.
6
+
7
+ See his accompanying blog posting at
8
+ http://www.pluitsolutions.com/2007/07/30/acts-as-ferret-drbserver-win32-service/
9
+
10
+
11
+ Usage
12
+ =====
13
+
14
+ There are two scripts:
15
+
16
+ script/ferret_service is used to install/remove/start/stop the win32 service.
17
+
18
+ script/ferret_daemon is to be called by Win32 service to start/stop the
19
+ DRbServer.
20
+
21
+ Run 'ruby script/ferret_service -h' for more info.
22
+
23
+
data/doc/monit-example ADDED
@@ -0,0 +1,22 @@
1
+ # monit configuration snippet to watch the Ferret DRb server shipped with
2
+ # acts_as_ferret
3
+ check process ferret with pidfile /path/to/ferret.pid
4
+
5
+ # username is the user the drb server should be running as (It's good practice
6
+ # to run such services as a non-privileged user)
7
+ start program = "/bin/su -c 'cd /path/to/your/app/current/ && script/ferret_server -e production start' username"
8
+ stop program = "/bin/su -c 'cd /path/to/your/app/current/ && script/ferret_server -e production stop' username"
9
+
10
+ # cpu usage boundaries
11
+ if cpu > 60% for 2 cycles then alert
12
+ if cpu > 90% for 5 cycles then restart
13
+
14
+ # memory usage varies with index size and usage scenarios, so check how
15
+ # much memory your DRb server uses up usually and add some spare to that
16
+ # before enabling this rule:
17
+ # if totalmem > 50.0 MB for 5 cycles then restart
18
+
19
+ # adjust port numbers according to your setup:
20
+ if failed port 9010 then alert
21
+ if failed port 9010 for 2 cycles then restart
22
+ group ferret
data/install.rb CHANGED
@@ -11,8 +11,7 @@ def install(file)
11
11
  end
12
12
  end
13
13
 
14
- install File.join( 'script', 'ferret_start' )
15
- install File.join( 'script', 'ferret_stop' )
14
+ install File.join( 'script', 'ferret_server' )
16
15
  install File.join( 'config', 'ferret_server.yml' )
17
16
 
18
17
  puts IO.read(File.join(File.dirname(__FILE__), 'README'))
data/lib/act_methods.rb CHANGED
@@ -38,6 +38,9 @@ module ActsAsFerret #:nodoc:
38
38
  # named class_name
39
39
  #
40
40
  # reindex_batch_size:: reindexing is done in batches of this size, default is 1000
41
+ # mysql_fast_batches:: set this to false to disable the faster mysql batching
42
+ # algorithm if this model uses a non-integer primary key named
43
+ # 'id' on MySQL.
41
44
  #
42
45
  # ferret:: Hash of Options that directly influence the way the Ferret engine works. You
43
46
  # can use most of the options the Ferret::I class accepts here, too. Among the
@@ -63,6 +66,8 @@ module ActsAsFerret #:nodoc:
63
66
  # For downwards compatibility reasons you can also specify the Ferret options in the
64
67
  # last Hash argument.
65
68
  def acts_as_ferret(options={}, ferret_options={})
69
+ # default to DRb mode
70
+ options[:remote] = true if options[:remote].nil?
66
71
 
67
72
  # force local mode if running *inside* the Ferret server - somewhere the
68
73
  # real indexing has to be done after all :-)
@@ -71,14 +76,14 @@ module ActsAsFerret #:nodoc:
71
76
  # DRb server is started, so this code is executed too early and detection won't
72
77
  # work. In this case you'll get endless loops resulting in "stack level too deep"
73
78
  # errors.
74
- # To get around this, start the server with the environment variable
79
+ # To get around this, start the DRb server with the environment variable
75
80
  # FERRET_USE_LOCAL_INDEX set to '1'.
76
81
  logger.debug "Asked for a remote server ? #{options[:remote].inspect}, ENV[\"FERRET_USE_LOCAL_INDEX\"] is #{ENV["FERRET_USE_LOCAL_INDEX"].inspect}, looks like we are#{ActsAsFerret::Remote::Server.running || ENV['FERRET_USE_LOCAL_INDEX'] ? '' : ' not'} the server"
77
82
  options.delete(:remote) if ENV["FERRET_USE_LOCAL_INDEX"] || ActsAsFerret::Remote::Server.running
78
83
 
79
84
  if options[:remote] && options[:remote] !~ /^druby/
80
85
  # read server location from config/ferret_server.yml
81
- options[:remote] = ActsAsFerret::Remote::Config.load("#{RAILS_ROOT}/config/ferret_server.yml")[:uri] rescue nil
86
+ options[:remote] = ActsAsFerret::Remote::Config.new.uri rescue nil
82
87
  end
83
88
 
84
89
  if options[:remote]
@@ -110,7 +115,9 @@ module ActsAsFerret #:nodoc:
110
115
  :single_index => false,
111
116
  :reindex_batch_size => 1000,
112
117
  :ferret => {}, # Ferret config Hash
113
- :ferret_fields => {} # list of indexed fields that will be filled later
118
+ :ferret_fields => {}, # list of indexed fields that will be filled later
119
+ :enabled => true, # used for class-wide disabling of Ferret
120
+ :mysql_fast_batches => true # turn off to disable the faster, id based batching mechanism for MySQL
114
121
  }
115
122
 
116
123
  # merge aaf options with args
@@ -206,6 +213,10 @@ module ActsAsFerret #:nodoc:
206
213
  # helper that defines a method that adds the given field to a ferret
207
214
  # document instance
208
215
  def define_to_field_method(field, options = {})
216
+ if options[:boost].is_a?(Symbol)
217
+ dynamic_boost = options[:boost]
218
+ options.delete :boost
219
+ end
209
220
  options.reverse_merge!( :store => :no,
210
221
  :highlight => :yes,
211
222
  :index => :yes,
@@ -213,9 +224,10 @@ module ActsAsFerret #:nodoc:
213
224
  :boost => 1.0 )
214
225
  options[:term_vector] = :no if options[:index] == :no
215
226
  aaf_configuration[:ferret_fields][field] = options
227
+
216
228
  define_method("#{field}_to_ferret".to_sym) do
217
229
  begin
218
- val = content_for_field_name(field)
230
+ val = content_for_field_name(field, dynamic_boost)
219
231
  rescue
220
232
  logger.warn("Error retrieving value for field #{field}: #{$!}")
221
233
  val = ''
@@ -21,10 +21,13 @@
21
21
  require 'active_support'
22
22
  require 'active_record'
23
23
  require 'set'
24
+ require 'enumerator'
24
25
  require 'ferret'
25
26
 
27
+ require 'bulk_indexer'
26
28
  require 'ferret_extensions'
27
29
  require 'act_methods'
30
+ require 'search_results'
28
31
  require 'class_methods'
29
32
  require 'shared_index_class_methods'
30
33
  require 'ferret_result'
@@ -66,32 +69,18 @@ require 'ferret_server'
66
69
  #
67
70
  module ActsAsFerret
68
71
 
69
- # global Hash containing all multi indexes created by all classes using the plugin
70
- # key is the concatenation of alphabetically sorted names of the classes the
71
- # searcher searches.
72
- @@multi_indexes = Hash.new
73
- def self.multi_indexes; @@multi_indexes end
72
+ # global Hash containing all multi indexes created by all classes using the plugin
73
+ # key is the concatenation of alphabetically sorted names of the classes the
74
+ # searcher searches.
75
+ @@multi_indexes = Hash.new
76
+ def self.multi_indexes; @@multi_indexes end
74
77
 
75
- # global Hash containing the ferret indexes of all classes using the plugin
76
- # key is the index directory.
77
- @@ferret_indexes = Hash.new
78
- def self.ferret_indexes; @@ferret_indexes end
78
+ # global Hash containing the ferret indexes of all classes using the plugin
79
+ # key is the index directory.
80
+ @@ferret_indexes = Hash.new
81
+ def self.ferret_indexes; @@ferret_indexes end
79
82
 
80
83
 
81
- # decorator that adds a total_hits accessor to search result arrays
82
- class SearchResults
83
- attr_reader :total_hits
84
- def initialize(results, total_hits)
85
- @results = results
86
- @total_hits = total_hits
87
- end
88
- def method_missing(symbol, *args, &block)
89
- @results.send(symbol, *args, &block)
90
- end
91
- def respond_to?(name)
92
- self.methods.include?(name) || @results.respond_to?(name)
93
- end
94
- end
95
84
 
96
85
  def self.ensure_directory(dir)
97
86
  FileUtils.mkdir_p dir unless (File.directory?(dir) || File.symlink?(dir))
@@ -133,6 +122,8 @@ module ActsAsFerret
133
122
  end
134
123
  end
135
124
  fields.each_pair do |field, options|
125
+ options = options.dup
126
+ options.delete(:boost) if options[:boost].is_a?(Symbol)
136
127
  fi.add_field(field, { :store => :no,
137
128
  :index => :yes }.update(options))
138
129
  end
@@ -0,0 +1,35 @@
1
+ module ActsAsFerret
2
+ class BulkIndexer
3
+ def initialize(args = {})
4
+ @batch_size = args[:batch_size] || 1000
5
+ @logger = args[:logger]
6
+ @model = args[:model]
7
+ @work_done = 0
8
+ @index = args[:index]
9
+ if args[:reindex]
10
+ @reindex = true
11
+ @model_count = @model.count.to_f
12
+ else
13
+ @model_count = args[:total]
14
+ end
15
+ end
16
+
17
+ def index_records(records, offset)
18
+ batch_time = measure_time {
19
+ records.each { |rec| @index << rec.to_doc if rec.ferret_enabled?(true) }
20
+ }.to_f
21
+ @work_done = offset.to_f / @model_count * 100.0 if @model_count > 0
22
+ remaining_time = ( batch_time / @batch_size ) * ( @model_count - offset + @batch_size )
23
+ @logger.info "#{@reindex ? 're' : 'bulk '}index model #{@model.name} : #{'%.2f' % @work_done}% complete : #{'%.2f' % remaining_time} secs to finish"
24
+
25
+ end
26
+
27
+ def measure_time
28
+ t1 = Time.now
29
+ yield
30
+ Time.now - t1
31
+ end
32
+
33
+ end
34
+
35
+ end
data/lib/class_methods.rb CHANGED
@@ -2,6 +2,24 @@ module ActsAsFerret
2
2
 
3
3
  module ClassMethods
4
4
 
5
+ # Disables ferret index updates for this model. When a block is given,
6
+ # Ferret will be re-enabled again after executing the block.
7
+ def disable_ferret
8
+ aaf_configuration[:enabled] = false
9
+ if block_given?
10
+ yield
11
+ enable_ferret
12
+ end
13
+ end
14
+
15
+ def enable_ferret
16
+ aaf_configuration[:enabled] = true
17
+ end
18
+
19
+ def ferret_enabled?
20
+ aaf_configuration[:enabled]
21
+ end
22
+
5
23
  # rebuild the index from all data stored for this model.
6
24
  # This is called automatically when no index exists yet.
7
25
  #
@@ -16,13 +34,34 @@ module ActsAsFerret
16
34
  index_dir = find_last_index_version(aaf_configuration[:index_base_dir]) unless aaf_configuration[:remote]
17
35
  end
18
36
 
37
+ # re-index a number records specified by the given ids. Use for large
38
+ # indexing jobs i.e. after modifying a lot of records with Ferret disabled.
39
+ # Please note that the state of Ferret (enabled or disabled at class or
40
+ # record level) is not checked by this method, so if you need to do so
41
+ # (e.g. because of a custom ferret_enabled? implementation), you have to do
42
+ # so yourself.
43
+ def bulk_index(*ids)
44
+ options = Hash === ids.last ? ids.pop : {}
45
+ ids = ids.first if ids.size == 1 && ids.first.is_a?(Enumerable)
46
+ aaf_index.bulk_index(ids, options)
47
+ end
48
+
49
+ # true if our db and table appear to be suitable for the mysql fast batch
50
+ # hack (see
51
+ # http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord)
52
+ def use_fast_batches?
53
+ if connection.class.name =~ /Mysql/ && primary_key == 'id' && aaf_configuration[:mysql_fast_batches]
54
+ logger.info "using mysql specific batched find :all. Turn off with :mysql_fast_batches => false if you encounter problems (i.e. because of non-integer UUIDs in the id column)"
55
+ true
56
+ end
57
+ end
58
+
19
59
  # runs across all records yielding those to be indexed when the index is rebuilt
20
60
  def records_for_rebuild(batch_size = 1000)
21
61
  transaction do
22
- if connection.class.name =~ /Mysql/ && primary_key == 'id'
23
- logger.info "using mysql specific batched find :all"
62
+ if use_fast_batches?
24
63
  offset = 0
25
- while (rows = find :all, :conditions => ["id > ?", offset ], :limit => batch_size).any?
64
+ while (rows = find :all, :conditions => [ "#{table_name}.id > ?", offset ], :limit => batch_size).any?
26
65
  offset = rows.last.id
27
66
  yield rows, offset
28
67
  end
@@ -36,6 +75,21 @@ module ActsAsFerret
36
75
  end
37
76
  end
38
77
 
78
+ # yields the records with the given ids, in batches of batch_size
79
+ def records_for_bulk_index(ids, batch_size = 1000)
80
+ transaction do
81
+ offset = 0
82
+ ids.each_slice(batch_size) do |id_slice|
83
+ logger.debug "########## slice: #{id_slice.join(',')}"
84
+ records = find( :all, :conditions => ["id in (?)", id_slice] )
85
+ logger.debug "########## slice records: #{records.inspect}"
86
+ #yield records, offset
87
+ yield find( :all, :conditions => ["id in (?)", id_slice] ), offset
88
+ offset += batch_size
89
+ end
90
+ end
91
+ end
92
+
39
93
  # Switches this class to a new index located in dir.
40
94
  # Used by the DRb server when switching to a new index version.
41
95
  def index_dir=(dir)
@@ -59,7 +113,15 @@ module ActsAsFerret
59
113
  # OR between terms for ORed queries. Or specify +:or_default => true+ in the
60
114
  # +:ferret+ options hash of acts_as_ferret.
61
115
  #
116
+ # You may either use the +offset+ and +limit+ options to implement your own
117
+ # pagination logic, or use the +page+ and +per_page+ options to use the
118
+ # built in pagination support which is compatible with will_paginate's view
119
+ # helpers. If +page+ and +per_page+ are given, +offset+ and +limit+ will be
120
+ # ignored.
121
+ #
62
122
  # == options:
123
+ # page:: page of search results to retrieve
124
+ # per_page:: number of search results that are displayed per page
63
125
  # offset:: first hit to retrieve (useful for paging)
64
126
  # limit:: number of hits to retrieve, or :all to retrieve
65
127
  # all results
@@ -71,6 +133,10 @@ module ActsAsFerret
71
133
  # work here)
72
134
  # models:: only for single_index scenarios: an Array of other Model classes to
73
135
  # include in this search. Use :all to query all models.
136
+ # multi:: Specify additional model classes to search through. Each of
137
+ # these, as well as this class, has to have the
138
+ # :store_class_name option set to true. This option replaces the
139
+ # multi_search method.
74
140
  #
75
141
  # +find_options+ is a hash passed on to active_record's find when
76
142
  # retrieving the data from db, useful to i.e. prefetch relationships with
@@ -78,25 +144,69 @@ module ActsAsFerret
78
144
  #
79
145
  # This method returns a +SearchResults+ instance, which really is an Array that has
80
146
  # been decorated with a total_hits attribute holding the total number of hits.
147
+ # Additionally, SearchResults is compatible with the pagination helper
148
+ # methods of the will_paginate plugin.
81
149
  #
82
- # Please keep in mind that the number of total hits might be wrong if you specify
83
- # both ferret options and active record find_options that somehow limit the result
84
- # set (e.g. +:num_docs+ and some +:conditions+).
150
+ # Please keep in mind that the number of results delivered might be less than
151
+ # +limit+ if you specify any active record conditions that further limit
152
+ # the result. Use +limit+ and +offset+ as AR find_options instead.
153
+ # +page+ and +per_page+ are supposed to work regardless of any
154
+ # +conitions+ present in +find_options+.
85
155
  def find_with_ferret(q, options = {}, find_options = {})
86
- total_hits, result = find_records_lazy_or_not q, options, find_options
156
+ if options[:per_page]
157
+ options[:page] = options[:page] ? options[:page].to_i : 1
158
+ limit = options[:per_page]
159
+ offset = (options[:page] - 1) * limit
160
+ if find_options[:conditions] && !options[:multi]
161
+ find_options[:limit] = limit
162
+ find_options[:offset] = offset
163
+ options[:limit] = :all
164
+ options.delete :offset
165
+ else
166
+ # do pagination with ferret (or after everything is done in the case
167
+ # of multi_search)
168
+ options[:limit] = limit
169
+ options[:offset] = offset
170
+ end
171
+ elsif find_options[:conditions]
172
+ if options[:multi]
173
+ # multisearch ignores find_options limit and offset
174
+ options[:limit] ||= find_options.delete(:limit)
175
+ options[:offset] ||= find_options.delete(:offset)
176
+ else
177
+ # let the db do the limiting and offsetting for single-table searches
178
+ find_options[:limit] ||= options.delete(:limit)
179
+ find_options[:offset] ||= options.delete(:offset)
180
+ options[:limit] = :all
181
+ end
182
+ end
183
+
184
+ total_hits, result = if options[:multi].blank?
185
+ find_records_lazy_or_not q, options, find_options
186
+ else
187
+ _multi_search q, options.delete(:multi), options, find_options
188
+ end
87
189
  logger.debug "Query: #{q}\ntotal hits: #{total_hits}, results delivered: #{result.size}"
88
- return SearchResults.new(result, total_hits)
190
+ SearchResults.new(result, total_hits, options[:page], options[:per_page])
89
191
  end
90
192
  alias find_by_contents find_with_ferret
91
193
 
92
194
 
93
195
 
94
196
  # Returns the total number of hits for the given query
95
- # To count the results of a multi_search query, specify an array of
96
- # class names with the :models option.
197
+ # To count the results of a query across multiple models, specify an array of
198
+ # class names with the :multi option.
199
+ #
200
+ # Note that since we don't query the database here, this method won't deliver
201
+ # the expected results when used on an AR association.
97
202
  def total_hits(q, options={})
98
- if models = options[:models]
99
- options[:models] = add_self_to_model_list_if_necessary(models).map(&:to_s)
203
+ if options[:models]
204
+ # backwards compatibility
205
+ logger.warn "the :models option of total_hits is deprecated, please use :multi instead"
206
+ options[:multi] = options[:models]
207
+ end
208
+ if models = options[:multi]
209
+ options[:multi] = add_self_to_model_list_if_necessary(models).map(&:to_s)
100
210
  end
101
211
  aaf_index.total_hits(q, options)
102
212
  end
@@ -120,9 +230,22 @@ module ActsAsFerret
120
230
  aaf_index.find_id_by_contents(q, options, &block)
121
231
  end
122
232
 
123
- # requires the store_class_name option of acts_as_ferret to be true
124
- # for all models queried this way.
125
- def multi_search(query, additional_models = [], options = {}, find_options = {})
233
+
234
+ # returns an array of hashes, each containing :class_name,
235
+ # :id and :score for a hit.
236
+ #
237
+ # if a block is given, class_name, id and score of each hit will
238
+ # be yielded, and the total number of hits is returned.
239
+ def id_multi_search(query, additional_models = [], options = {}, &proc)
240
+ deprecated_options_support(options)
241
+ models = add_self_to_model_list_if_necessary(additional_models)
242
+ aaf_index.id_multi_search(query, models.map(&:to_s), options, &proc)
243
+ end
244
+
245
+
246
+ protected
247
+
248
+ def _multi_search(query, additional_models = [], options = {}, find_options = {})
126
249
  result = []
127
250
 
128
251
  if options[:lazy]
@@ -133,33 +256,28 @@ module ActsAsFerret
133
256
  else
134
257
  id_arrays = {}
135
258
  rank = 0
259
+
260
+ limit = options.delete(:limit)
261
+ offset = options.delete(:offset) || 0
262
+ options[:limit] = :all
136
263
  total_hits = id_multi_search(query, additional_models, options) do |model, id, score, data|
137
264
  id_arrays[model] ||= {}
138
265
  id_arrays[model][id] = [ rank += 1, score ]
139
266
  end
140
267
  result = retrieve_records(id_arrays, find_options)
268
+ total_hits = result.size if find_options[:conditions]
269
+ # total_hits += offset if offset
270
+ if limit && limit != :all
271
+ result = result[offset..limit+offset-1]
272
+ end
141
273
  end
142
-
143
- SearchResults.new(result, total_hits)
274
+ [total_hits, result]
144
275
  end
145
-
146
- # returns an array of hashes, each containing :class_name,
147
- # :id and :score for a hit.
148
- #
149
- # if a block is given, class_name, id and score of each hit will
150
- # be yielded, and the total number of hits is returned.
151
- def id_multi_search(query, additional_models = [], options = {}, &proc)
152
- deprecated_options_support(options)
153
- additional_models = add_self_to_model_list_if_necessary(additional_models)
154
- aaf_index.id_multi_search(query, additional_models.map(&:to_s), options, &proc)
155
- end
156
-
157
-
158
- protected
159
276
 
160
277
  def add_self_to_model_list_if_necessary(models)
161
278
  models = [ models ] unless models.is_a? Array
162
279
  models << self unless models.include?(self)
280
+ models
163
281
  end
164
282
 
165
283
  def find_records_lazy_or_not(q, options = {}, find_options = {})
@@ -174,23 +292,30 @@ module ActsAsFerret
174
292
  def ar_find_by_contents(q, options = {}, find_options = {})
175
293
  result_ids = {}
176
294
  total_hits = find_id_by_contents(q, options) do |model, id, score, data|
177
- # stores ids, index of each id for later ordering of
178
- # results, and score
295
+ # stores ids, index and score of each hit for later ordering of
296
+ # results
179
297
  result_ids[id] = [ result_ids.size + 1, score ]
180
298
  end
181
299
 
182
300
  result = retrieve_records( { self.name => result_ids }, find_options )
183
301
 
184
- if find_options[:conditions]
185
- if options[:limit] != :all
186
- # correct result size if the user specified conditions
187
- # wenn conditions: options[:limit] != :all --> ferret-query mit :all wiederholen und select count machen
302
+ # count total_hits via sql when using conditions or when we're called
303
+ # from an ActiveRecord association.
304
+ if find_options[:conditions] or caller.find{ |call| call =~ %r{active_record/associations} }
305
+ # chances are the ferret result count is not our total_hits value, so
306
+ # we correct this here.
307
+ if options[:limit] != :all || options[:page] || options[:offset] || find_options[:limit] || find_options[:offset]
308
+ # our ferret result has been limited, so we need to re-run that
309
+ # search to get the full result set from ferret.
188
310
  result_ids = {}
189
- find_id_by_contents(q, options.update(:limit => :all)) do |model, id, score, data|
311
+ find_id_by_contents(q, options.update(:limit => :all, :offset => 0)) do |model, id, score, data|
190
312
  result_ids[id] = [ result_ids.size + 1, score ]
191
313
  end
314
+ # Now ask the database for the total size of the final result set.
192
315
  total_hits = count_records( { self.name => result_ids }, find_options )
193
316
  else
317
+ # what we got from the database is our full result set, so take
318
+ # it's size
194
319
  total_hits = result.length
195
320
  end
196
321
  end
@@ -240,22 +365,24 @@ module ActsAsFerret
240
365
  raise "Please use ':store_class_name => true' if you want to use multi_search.\n#{$!}"
241
366
  end
242
367
 
368
+ # merge conditions
369
+ conditions = combine_conditions([ "#{model.table_name}.#{model.primary_key} in (?)",
370
+ id_array.keys ],
371
+ find_options[:conditions])
372
+
243
373
  # check for include association that might only exist on some models in case of multi_search
244
374
  filtered_include_options = []
245
375
  if include_options = find_options[:include]
376
+ include_options = [ include_options ] unless include_options.respond_to?(:each)
246
377
  include_options.each do |include_option|
247
378
  filtered_include_options << include_option if model.reflections.has_key?(include_option.is_a?(Hash) ? include_option.keys[0].to_sym : include_option.to_sym)
248
379
  end
249
380
  end
250
- filtered_include_options=nil if filtered_include_options.empty?
381
+ filtered_include_options = nil if filtered_include_options.empty?
251
382
 
252
383
  # fetch
253
- tmp_result = nil
254
- model.send(:with_scope, :find => find_options) do
255
- tmp_result = model.find( :all, :conditions => [
256
- "#{model.table_name}.#{model.primary_key} in (?)", id_array.keys ],
257
- :include => filtered_include_options )
258
- end
384
+ tmp_result = model.find(:all, find_options.merge(:conditions => conditions,
385
+ :include => filtered_include_options))
259
386
 
260
387
  # set scores and rank
261
388
  tmp_result.each do |record|
@@ -272,15 +399,20 @@ module ActsAsFerret
272
399
  end
273
400
 
274
401
  def count_records(id_arrays, find_options = {})
402
+ count_options = find_options.dup
403
+ count_options.delete :limit
404
+ count_options.delete :offset
275
405
  count = 0
276
406
  id_arrays.each do |model, id_array|
277
407
  next if id_array.empty?
278
408
  begin
279
409
  model = model.constantize
280
- model.send(:with_scope, :find => find_options) do
281
- count += model.count(:conditions => [ "#{model.table_name}.#{model.primary_key} in (?)",
282
- id_array.keys ])
283
- end
410
+ # merge conditions
411
+ conditions = combine_conditions([ "#{model.table_name}.#{model.primary_key} in (?)", id_array.keys ],
412
+ find_options[:conditions])
413
+ opts = find_options.merge :conditions => conditions
414
+ opts.delete :limit; opts.delete :offset
415
+ count += model.count opts
284
416
  rescue TypeError
285
417
  raise "#{model} must use :store_class_name option if you want to use multi_search against it.\n#{$!}"
286
418
  end
@@ -310,6 +442,17 @@ module ActsAsFerret
310
442
  end.new(aaf_configuration)
311
443
  end
312
444
 
445
+ # combine our conditions with those given by user, if any
446
+ def combine_conditions(conditions, additional_conditions = [])
447
+ returning conditions do
448
+ if additional_conditions && additional_conditions.any?
449
+ cust_opts = additional_conditions.respond_to?(:shift) ? additional_conditions.dup : [ additional_conditions ]
450
+ conditions.first << " and " << cust_opts.shift
451
+ conditions.concat(cust_opts)
452
+ end
453
+ end
454
+ end
455
+
313
456
  end
314
457
 
315
458
  end