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 +3 -0
- data/bin/aaf_install +23 -0
- data/config/ferret_server.yml +19 -8
- data/doc/README.win32 +23 -0
- data/doc/monit-example +22 -0
- data/install.rb +1 -2
- data/lib/act_methods.rb +16 -4
- data/lib/acts_as_ferret.rb +14 -23
- data/lib/bulk_indexer.rb +35 -0
- data/lib/class_methods.rb +192 -49
- data/lib/ferret_extensions.rb +52 -18
- data/lib/ferret_server.rb +109 -37
- data/lib/instance_methods.rb +45 -15
- data/lib/local_index.rb +7 -5
- data/lib/search_results.rb +53 -0
- data/lib/server_manager.rb +44 -0
- data/lib/shared_index_class_methods.rb +1 -1
- data/lib/unix_daemon.rb +63 -0
- data/rakefile +5 -2
- data/script/ferret_daemon +94 -0
- data/script/ferret_server +1 -17
- data/script/ferret_service +178 -0
- metadata +34 -26
- data/script/ferret_start +0 -72
- data/script/ferret_stop +0 -26
data/README
CHANGED
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
|
+
|
data/config/ferret_server.yml
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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', '
|
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.
|
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 => {}
|
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 = ''
|
data/lib/acts_as_ferret.rb
CHANGED
@@ -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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
data/lib/bulk_indexer.rb
ADDED
@@ -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
|
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
|
83
|
-
#
|
84
|
-
#
|
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
|
-
|
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
|
-
|
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
|
96
|
-
# class names with the :
|
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
|
99
|
-
|
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
|
-
|
124
|
-
#
|
125
|
-
|
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
|
178
|
-
# results
|
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
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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 =
|
254
|
-
|
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
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|