acts_as_ferret 0.4.1 → 0.4.2

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/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