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