cehoffman-acts_as_ferret 0.4.4
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/LICENSE +20 -0
- data/README +68 -0
- data/bin/aaf_install +23 -0
- data/config/ferret_server.yml +24 -0
- data/doc/README.win32 +23 -0
- data/doc/demo/README +154 -0
- data/doc/demo/README_DEMO +23 -0
- data/doc/demo/Rakefile +10 -0
- data/doc/demo/app/controllers/admin/backend_controller.rb +14 -0
- data/doc/demo/app/controllers/admin_area_controller.rb +4 -0
- data/doc/demo/app/controllers/application.rb +5 -0
- data/doc/demo/app/controllers/contents_controller.rb +49 -0
- data/doc/demo/app/controllers/searches_controller.rb +8 -0
- data/doc/demo/app/helpers/admin/backend_helper.rb +2 -0
- data/doc/demo/app/helpers/application_helper.rb +3 -0
- data/doc/demo/app/helpers/content_helper.rb +2 -0
- data/doc/demo/app/helpers/search_helper.rb +2 -0
- data/doc/demo/app/models/comment.rb +48 -0
- data/doc/demo/app/models/content.rb +12 -0
- data/doc/demo/app/models/content_base.rb +28 -0
- data/doc/demo/app/models/search.rb +19 -0
- data/doc/demo/app/models/shared_index1.rb +3 -0
- data/doc/demo/app/models/shared_index2.rb +3 -0
- data/doc/demo/app/models/special_content.rb +3 -0
- data/doc/demo/app/models/stats.rb +20 -0
- data/doc/demo/app/views/admin/backend/search.rhtml +18 -0
- data/doc/demo/app/views/contents/_form.rhtml +10 -0
- data/doc/demo/app/views/contents/edit.rhtml +9 -0
- data/doc/demo/app/views/contents/index.rhtml +24 -0
- data/doc/demo/app/views/contents/new.rhtml +8 -0
- data/doc/demo/app/views/contents/show.rhtml +8 -0
- data/doc/demo/app/views/layouts/application.html.erb +17 -0
- data/doc/demo/app/views/searches/_content.html.erb +2 -0
- data/doc/demo/app/views/searches/search.html.erb +20 -0
- data/doc/demo/config/boot.rb +109 -0
- data/doc/demo/config/database.yml +38 -0
- data/doc/demo/config/environment.rb +69 -0
- data/doc/demo/config/environments/development.rb +16 -0
- data/doc/demo/config/environments/production.rb +19 -0
- data/doc/demo/config/environments/test.rb +21 -0
- data/doc/demo/config/ferret_server.yml +18 -0
- data/doc/demo/config/lighttpd.conf +40 -0
- data/doc/demo/config/routes.rb +9 -0
- data/doc/demo/db/development_structure.sql +15 -0
- data/doc/demo/db/migrate/001_initial_migration.rb +18 -0
- data/doc/demo/db/migrate/002_add_type_to_contents.rb +9 -0
- data/doc/demo/db/migrate/003_create_shared_index1s.rb +11 -0
- data/doc/demo/db/migrate/004_create_shared_index2s.rb +11 -0
- data/doc/demo/db/migrate/005_special_field.rb +9 -0
- data/doc/demo/db/migrate/006_create_stats.rb +15 -0
- data/doc/demo/db/schema.sql +18 -0
- data/doc/demo/doc/README_FOR_APP +2 -0
- data/doc/demo/doc/howto.txt +70 -0
- data/doc/demo/public/.htaccess +40 -0
- data/doc/demo/public/404.html +8 -0
- data/doc/demo/public/500.html +8 -0
- data/doc/demo/public/dispatch.cgi +10 -0
- data/doc/demo/public/dispatch.fcgi +24 -0
- data/doc/demo/public/dispatch.rb +10 -0
- data/doc/demo/public/favicon.ico +0 -0
- data/doc/demo/public/images/rails.png +0 -0
- data/doc/demo/public/index.html +277 -0
- data/doc/demo/public/robots.txt +1 -0
- data/doc/demo/public/stylesheets/scaffold.css +74 -0
- data/doc/demo/script/about +3 -0
- data/doc/demo/script/breakpointer +3 -0
- data/doc/demo/script/console +3 -0
- data/doc/demo/script/destroy +3 -0
- data/doc/demo/script/ferret_server +10 -0
- data/doc/demo/script/generate +3 -0
- data/doc/demo/script/performance/benchmarker +3 -0
- data/doc/demo/script/performance/profiler +3 -0
- data/doc/demo/script/plugin +3 -0
- data/doc/demo/script/process/inspector +3 -0
- data/doc/demo/script/process/reaper +3 -0
- data/doc/demo/script/process/spawner +3 -0
- data/doc/demo/script/process/spinner +3 -0
- data/doc/demo/script/runner +3 -0
- data/doc/demo/script/server +3 -0
- data/doc/demo/test/fixtures/comments.yml +12 -0
- data/doc/demo/test/fixtures/contents.yml +13 -0
- data/doc/demo/test/fixtures/remote_contents.yml +9 -0
- data/doc/demo/test/fixtures/shared_index1s.yml +7 -0
- data/doc/demo/test/fixtures/shared_index2s.yml +7 -0
- data/doc/demo/test/functional/admin/backend_controller_test.rb +35 -0
- data/doc/demo/test/functional/contents_controller_test.rb +81 -0
- data/doc/demo/test/functional/searches_controller_test.rb +71 -0
- data/doc/demo/test/smoke/drb_smoke_test.rb +321 -0
- data/doc/demo/test/smoke/process_stats.rb +21 -0
- data/doc/demo/test/test_helper.rb +30 -0
- data/doc/demo/test/unit/comment_test.rb +217 -0
- data/doc/demo/test/unit/content_test.rb +705 -0
- data/doc/demo/test/unit/ferret_result_test.rb +24 -0
- data/doc/demo/test/unit/multi_index_test.rb +329 -0
- data/doc/demo/test/unit/remote_index_test.rb +23 -0
- data/doc/demo/test/unit/shared_index1_test.rb +108 -0
- data/doc/demo/test/unit/shared_index2_test.rb +13 -0
- data/doc/demo/test/unit/sort_test.rb +21 -0
- data/doc/demo/test/unit/special_content_test.rb +25 -0
- data/doc/demo/vendor/plugins/will_paginate/LICENSE +18 -0
- data/doc/demo/vendor/plugins/will_paginate/README +108 -0
- data/doc/demo/vendor/plugins/will_paginate/Rakefile +23 -0
- data/doc/demo/vendor/plugins/will_paginate/init.rb +21 -0
- data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/collection.rb +45 -0
- data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb +44 -0
- data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/finder.rb +159 -0
- data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb +95 -0
- data/doc/demo/vendor/plugins/will_paginate/test/array_pagination_test.rb +23 -0
- data/doc/demo/vendor/plugins/will_paginate/test/boot.rb +27 -0
- data/doc/demo/vendor/plugins/will_paginate/test/console +10 -0
- data/doc/demo/vendor/plugins/will_paginate/test/finder_test.rb +219 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/admin.rb +3 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/companies.yml +24 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/company.rb +23 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/developer.rb +11 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml +13 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/project.rb +4 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/projects.yml +7 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/replies.yml +20 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/reply.rb +5 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/schema.sql +44 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/topic.rb +19 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/topics.yml +30 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/user.rb +2 -0
- data/doc/demo/vendor/plugins/will_paginate/test/fixtures/users.yml +35 -0
- data/doc/demo/vendor/plugins/will_paginate/test/helper.rb +42 -0
- data/doc/demo/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb +64 -0
- data/doc/demo/vendor/plugins/will_paginate/test/lib/load_fixtures.rb +10 -0
- data/doc/demo/vendor/plugins/will_paginate/test/pagination_test.rb +136 -0
- data/doc/monit-example +22 -0
- data/init.rb +24 -0
- data/install.rb +18 -0
- data/lib/act_methods.rb +147 -0
- data/lib/acts_as_ferret.rb +584 -0
- data/lib/ar_mysql_auto_reconnect_patch.rb +41 -0
- data/lib/blank_slate.rb +53 -0
- data/lib/bulk_indexer.rb +38 -0
- data/lib/class_methods.rb +270 -0
- data/lib/ferret_extensions.rb +188 -0
- data/lib/ferret_find_methods.rb +141 -0
- data/lib/ferret_result.rb +53 -0
- data/lib/ferret_server.rb +238 -0
- data/lib/index.rb +99 -0
- data/lib/instance_methods.rb +171 -0
- data/lib/local_index.rb +205 -0
- data/lib/more_like_this.rb +217 -0
- data/lib/multi_index.rb +126 -0
- data/lib/rdig_adapter.rb +148 -0
- data/lib/remote_functions.rb +23 -0
- data/lib/remote_index.rb +54 -0
- data/lib/remote_multi_index.rb +20 -0
- data/lib/search_results.rb +50 -0
- data/lib/server_manager.rb +58 -0
- data/lib/unix_daemon.rb +64 -0
- data/lib/without_ar.rb +52 -0
- data/rakefile +141 -0
- data/recipes/aaf_recipes.rb +114 -0
- data/script/ferret_daemon +94 -0
- data/script/ferret_server +10 -0
- data/script/ferret_service +178 -0
- data/tasks/ferret.rake +22 -0
- metadata +258 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module ActsAsFerret
|
|
2
|
+
# Ferret search logic common to single-class indexes, shared indexes and
|
|
3
|
+
# multi indexes.
|
|
4
|
+
module FerretFindMethods
|
|
5
|
+
|
|
6
|
+
def find_records(q, options = {}, ar_options = {})
|
|
7
|
+
late_pagination = options.delete :late_pagination
|
|
8
|
+
total_hits, result = if options[:lazy]
|
|
9
|
+
logger.warn "find_options #{ar_options} are ignored because :lazy => true" unless ar_options.empty?
|
|
10
|
+
lazy_find q, options
|
|
11
|
+
else
|
|
12
|
+
ar_find q, options, ar_options
|
|
13
|
+
end
|
|
14
|
+
if late_pagination
|
|
15
|
+
limit = late_pagination[:limit]
|
|
16
|
+
offset = late_pagination[:offset] || 0
|
|
17
|
+
end_index = limit == :all ? -1 : limit+offset-1
|
|
18
|
+
# puts "late pagination: #{offset} : #{end_index}"
|
|
19
|
+
result = result[offset..end_index]
|
|
20
|
+
end
|
|
21
|
+
return [total_hits, result]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def lazy_find(q, options = {})
|
|
25
|
+
logger.debug "lazy_find: #{q}"
|
|
26
|
+
result = []
|
|
27
|
+
rank = 0
|
|
28
|
+
total_hits = find_ids(q, options) do |model, id, score, data|
|
|
29
|
+
logger.debug "model: #{model}, id: #{id}, data: #{data}"
|
|
30
|
+
result << FerretResult.new(model, id, score, rank += 1, data)
|
|
31
|
+
end
|
|
32
|
+
[ total_hits, result ]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ar_find(q, options = {}, ar_options = {})
|
|
36
|
+
ferret_options = options.dup
|
|
37
|
+
if ar_options[:conditions] or ar_options[:order]
|
|
38
|
+
ferret_options[:limit] = :all
|
|
39
|
+
ferret_options.delete :offset
|
|
40
|
+
end
|
|
41
|
+
total_hits, id_arrays = find_id_model_arrays q, ferret_options
|
|
42
|
+
logger.debug "now retrieving records from AR with options: #{ar_options.inspect}"
|
|
43
|
+
result = ActsAsFerret::retrieve_records(id_arrays, ar_options)
|
|
44
|
+
logger.debug "#{result.size} results from AR: #{result.inspect}"
|
|
45
|
+
|
|
46
|
+
# count total_hits via sql when using conditions, multiple models, or when we're called
|
|
47
|
+
# from an ActiveRecord association.
|
|
48
|
+
if id_arrays.size > 1 or ar_options[:conditions]
|
|
49
|
+
# chances are the ferret result count is not our total_hits value, so
|
|
50
|
+
# we correct this here.
|
|
51
|
+
if options[:limit] != :all || options[:page] || options[:offset] || ar_options[:limit] || ar_options[:offset]
|
|
52
|
+
# our ferret result has been limited, so we need to re-run that
|
|
53
|
+
# search to get the full result set from ferret.
|
|
54
|
+
new_th, id_arrays = find_id_model_arrays( q, options.merge(:limit => :all, :offset => 0) )
|
|
55
|
+
# Now ask the database for the total size of the final result set.
|
|
56
|
+
total_hits = count_records( id_arrays, ar_options )
|
|
57
|
+
else
|
|
58
|
+
# what we got from the database is our full result set, so take
|
|
59
|
+
# it's size
|
|
60
|
+
total_hits = result.length
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
[ total_hits, result ]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def count_records(id_arrays, ar_options = {})
|
|
67
|
+
count_options = ar_options.dup
|
|
68
|
+
count_options.delete :limit
|
|
69
|
+
count_options.delete :offset
|
|
70
|
+
count_options.delete :order
|
|
71
|
+
count = 0
|
|
72
|
+
id_arrays.each do |model, id_array|
|
|
73
|
+
next if id_array.empty?
|
|
74
|
+
model = model.constantize
|
|
75
|
+
# merge conditions
|
|
76
|
+
conditions = ActsAsFerret::conditions_for_model model, ar_options[:conditions]
|
|
77
|
+
count_options[:conditions] = ActsAsFerret::combine_conditions([ "#{model.table_name}.#{model.primary_key} in (?)", id_array.keys ], conditions)
|
|
78
|
+
count_options[:include] = ActsAsFerret::filter_include_list_for_model(model, ar_options[:include]) if ar_options[:include]
|
|
79
|
+
cnt = model.count count_options
|
|
80
|
+
if cnt.is_a?(ActiveSupport::OrderedHash) # fixes #227
|
|
81
|
+
count += cnt.size
|
|
82
|
+
else
|
|
83
|
+
count += cnt
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
count
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def find_id_model_arrays(q, options)
|
|
90
|
+
id_arrays = {}
|
|
91
|
+
rank = 0
|
|
92
|
+
total_hits = find_ids(q, options) do |model, id, score, data|
|
|
93
|
+
id_arrays[model] ||= {}
|
|
94
|
+
id_arrays[model][id] = [ rank += 1, score ]
|
|
95
|
+
end
|
|
96
|
+
[total_hits, id_arrays]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Queries the Ferret index to retrieve model class, id, score and the
|
|
100
|
+
# values of any fields stored in the index for each hit.
|
|
101
|
+
# If a block is given, these are yielded and the number of total hits is
|
|
102
|
+
# returned. Otherwise [total_hits, result_array] is returned.
|
|
103
|
+
def find_ids(query, options = {})
|
|
104
|
+
|
|
105
|
+
result = []
|
|
106
|
+
stored_fields = determine_stored_fields options
|
|
107
|
+
|
|
108
|
+
q = process_query(query, options)
|
|
109
|
+
q = scope_query_to_models q, options[:models] #if shared?
|
|
110
|
+
logger.debug "query: #{query}\n-->#{q}"
|
|
111
|
+
s = searcher
|
|
112
|
+
total_hits = s.search_each(q, options) do |hit, score|
|
|
113
|
+
doc = s[hit]
|
|
114
|
+
model = doc[:class_name]
|
|
115
|
+
# fetch stored fields if lazy loading
|
|
116
|
+
data = extract_stored_fields(doc, stored_fields)
|
|
117
|
+
if block_given?
|
|
118
|
+
yield model, doc[:id], score, data
|
|
119
|
+
else
|
|
120
|
+
result << { :model => model, :id => doc[:id], :score => score, :data => data }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
#logger.debug "id_score_model array: #{result.inspect}"
|
|
124
|
+
return block_given? ? total_hits : [total_hits, result]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def scope_query_to_models(query, models)
|
|
128
|
+
return query if models.nil? or models == :all
|
|
129
|
+
models = [ models ] if Class === models
|
|
130
|
+
q = Ferret::Search::BooleanQuery.new
|
|
131
|
+
q.add_query(query, :must)
|
|
132
|
+
model_query = Ferret::Search::BooleanQuery.new
|
|
133
|
+
models.each do |model|
|
|
134
|
+
model_query.add_query(Ferret::Search::TermQuery.new(:class_name, model.name), :should)
|
|
135
|
+
end
|
|
136
|
+
q.add_query(model_query, :must)
|
|
137
|
+
return q
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module ActsAsFerret
|
|
2
|
+
|
|
3
|
+
# mixed into the FerretResult and AR classes calling acts_as_ferret
|
|
4
|
+
module ResultAttributes
|
|
5
|
+
# holds the score this record had when it was found via
|
|
6
|
+
# acts_as_ferret
|
|
7
|
+
attr_accessor :ferret_score
|
|
8
|
+
|
|
9
|
+
attr_accessor :ferret_rank
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class FerretResult < ActsAsFerret::BlankSlate
|
|
13
|
+
include ResultAttributes
|
|
14
|
+
attr_accessor :id
|
|
15
|
+
reveal :methods
|
|
16
|
+
|
|
17
|
+
def initialize(model, id, score, rank, data = {})
|
|
18
|
+
@model = model.constantize
|
|
19
|
+
@id = id
|
|
20
|
+
@ferret_score = score
|
|
21
|
+
@ferret_rank = rank
|
|
22
|
+
@data = data
|
|
23
|
+
@use_record = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inspect
|
|
27
|
+
"#<FerretResult wrapper for #{@model} with id #{@id}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def method_missing(method, *args, &block)
|
|
31
|
+
if (@ar_record && @use_record) || !@data.has_key?(method)
|
|
32
|
+
to_record.send method, *args, &block
|
|
33
|
+
else
|
|
34
|
+
@data[method]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def respond_to?(name)
|
|
39
|
+
methods.include?(name.to_s) || @data.has_key?(name.to_sym) || to_record.respond_to?(name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_record
|
|
43
|
+
unless @ar_record
|
|
44
|
+
@ar_record = @model.find(id)
|
|
45
|
+
@ar_record.ferret_rank = ferret_rank
|
|
46
|
+
@ar_record.ferret_score = ferret_score
|
|
47
|
+
# don't try to fetch attributes from RDig based records
|
|
48
|
+
@use_record = !@ar_record.class.included_modules.include?(ActsAsFerret::RdigAdapter)
|
|
49
|
+
end
|
|
50
|
+
@ar_record
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
require 'drb'
|
|
2
|
+
require 'thread'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'erb'
|
|
5
|
+
|
|
6
|
+
################################################################################
|
|
7
|
+
module ActsAsFerret
|
|
8
|
+
module Remote
|
|
9
|
+
|
|
10
|
+
################################################################################
|
|
11
|
+
class Config
|
|
12
|
+
|
|
13
|
+
################################################################################
|
|
14
|
+
DEFAULTS = {
|
|
15
|
+
'host' => 'localhost',
|
|
16
|
+
'port' => '9009',
|
|
17
|
+
'cf' => "#{RAILS_ROOT}/config/ferret_server.yml",
|
|
18
|
+
'pid_file' => "#{RAILS_ROOT}/log/ferret_server.pid",
|
|
19
|
+
'log_file' => "#{RAILS_ROOT}/log/ferret_server.log",
|
|
20
|
+
'log_level' => 'debug',
|
|
21
|
+
'socket' => nil,
|
|
22
|
+
'script' => nil
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
################################################################################
|
|
26
|
+
# load the configuration file and apply default settings
|
|
27
|
+
def initialize (file=DEFAULTS['cf'])
|
|
28
|
+
@everything = YAML.load(ERB.new(IO.read(file)).result)
|
|
29
|
+
raise "malformed ferret server config" unless @everything.is_a?(Hash)
|
|
30
|
+
@config = DEFAULTS.merge(@everything[RAILS_ENV] || {})
|
|
31
|
+
if @everything[RAILS_ENV]
|
|
32
|
+
@config['uri'] = socket.nil? ? "druby://#{host}:#{port}" : "drbunix:#{socket}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
################################################################################
|
|
37
|
+
# treat the keys of the config data as methods
|
|
38
|
+
def method_missing (name, *args)
|
|
39
|
+
@config.has_key?(name.to_s) ? @config[name.to_s] : super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#################################################################################
|
|
45
|
+
# This class acts as a drb server listening for indexing and
|
|
46
|
+
# search requests from models declared to 'acts_as_ferret :remote => true'
|
|
47
|
+
#
|
|
48
|
+
# Usage:
|
|
49
|
+
# - modify RAILS_ROOT/config/ferret_server.yml to suit your needs.
|
|
50
|
+
# - environments for which no section in the config file exists will use
|
|
51
|
+
# the index locally (good for unit tests/development mode)
|
|
52
|
+
# - run script/ferret_server to start the server:
|
|
53
|
+
# script/ferret_server -e production start
|
|
54
|
+
# - to stop the server run
|
|
55
|
+
# script/ferret_server -e production stop
|
|
56
|
+
#
|
|
57
|
+
class Server
|
|
58
|
+
|
|
59
|
+
#################################################################################
|
|
60
|
+
# FIXME include detection of OS and include the correct file
|
|
61
|
+
require 'unix_daemon'
|
|
62
|
+
include(ActsAsFerret::Remote::UnixDaemon)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
################################################################################
|
|
66
|
+
cattr_accessor :running
|
|
67
|
+
|
|
68
|
+
################################################################################
|
|
69
|
+
def initialize
|
|
70
|
+
ActiveRecord::Base.allow_concurrency = true
|
|
71
|
+
require 'ar_mysql_auto_reconnect_patch'
|
|
72
|
+
@cfg = ActsAsFerret::Remote::Config.new
|
|
73
|
+
ActiveRecord::Base.logger = @logger = Logger.new(@cfg.log_file)
|
|
74
|
+
ActiveRecord::Base.logger.level = Logger.const_get(@cfg.log_level.upcase) rescue Logger::DEBUG
|
|
75
|
+
if @cfg.script
|
|
76
|
+
path = File.join(RAILS_ROOT, @cfg.script)
|
|
77
|
+
load path
|
|
78
|
+
@logger.info "loaded custom startup script from #{path}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
################################################################################
|
|
83
|
+
# start the server as a daemon process
|
|
84
|
+
def start
|
|
85
|
+
raise "ferret_server not configured for #{RAILS_ENV}" unless (@cfg.uri rescue nil)
|
|
86
|
+
platform_daemon { run_drb_service }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
################################################################################
|
|
90
|
+
# run the server and block until it exits
|
|
91
|
+
def run
|
|
92
|
+
raise "ferret_server not configured for #{RAILS_ENV}" unless (@cfg.uri rescue nil)
|
|
93
|
+
run_drb_service
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def run_drb_service
|
|
97
|
+
$stdout.puts("starting ferret server...")
|
|
98
|
+
self.class.running = true
|
|
99
|
+
DRb.start_service(@cfg.uri, self)
|
|
100
|
+
DRb.thread.join
|
|
101
|
+
rescue Exception => e
|
|
102
|
+
@logger.error(e.to_s)
|
|
103
|
+
raise
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
#################################################################################
|
|
107
|
+
# handles all incoming method calls, and sends them on to the correct local index
|
|
108
|
+
# instance.
|
|
109
|
+
#
|
|
110
|
+
# Calls are not queued, so this will block until the call returned.
|
|
111
|
+
#
|
|
112
|
+
def method_missing(name, *args)
|
|
113
|
+
@logger.debug "\#method_missing(#{name.inspect}, #{args.inspect})"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
index_name = args.shift
|
|
117
|
+
index = if name.to_s =~ /^multi_(.+)/
|
|
118
|
+
name = $1
|
|
119
|
+
ActsAsFerret::multi_index(index_name)
|
|
120
|
+
else
|
|
121
|
+
ActsAsFerret::get_index(index_name)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if index.nil?
|
|
125
|
+
@logger.error "\#index with name #{index_name} not found in call to #{name} with args #{args.inspect}"
|
|
126
|
+
raise ActsAsFerret::IndexNotDefined.new(index_name)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# TODO find another way to implement the reconnection logic (maybe in
|
|
131
|
+
# local_index or class_methods)
|
|
132
|
+
# reconnect_when_needed(clazz) do
|
|
133
|
+
|
|
134
|
+
# using respond_to? here so we not have to catch NoMethodError
|
|
135
|
+
# which would silently catch those from deep inside the indexing
|
|
136
|
+
# code, too...
|
|
137
|
+
|
|
138
|
+
if index.respond_to?(name)
|
|
139
|
+
index.send name, *args
|
|
140
|
+
# TODO check where we need this:
|
|
141
|
+
#elsif clazz.respond_to?(name)
|
|
142
|
+
# @logger.debug "no luck, trying to call class method instead"
|
|
143
|
+
# clazz.send name, *args
|
|
144
|
+
else
|
|
145
|
+
raise NoMethodError.new("method #{name} not supported by DRb server")
|
|
146
|
+
end
|
|
147
|
+
rescue => e
|
|
148
|
+
@logger.error "ferret server error #{$!}\n#{$!.backtrace.join "\n"}"
|
|
149
|
+
raise e
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def register_class(class_name)
|
|
153
|
+
@logger.debug "############ registerclass #{class_name}"
|
|
154
|
+
class_name.constantize
|
|
155
|
+
@logger.debug "index for class #{class_name}: #{ActsAsFerret::ferret_indexes[class_name.underscore.to_sym]}"
|
|
156
|
+
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# make sure we have a versioned index in place, building one if necessary
|
|
160
|
+
def ensure_index_exists(index_name)
|
|
161
|
+
@logger.debug "DRb server: ensure_index_exists for index #{index_name}"
|
|
162
|
+
definition = ActsAsFerret::get_index(index_name).index_definition
|
|
163
|
+
dir = definition[:index_dir]
|
|
164
|
+
unless File.directory?(dir) && File.file?(File.join(dir, 'segments')) && dir =~ %r{/\d+(_\d+)?$}
|
|
165
|
+
rebuild_index(index_name)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# disconnects the db connection for the class specified by class_name
|
|
170
|
+
# used only in unit tests to check the automatic reconnection feature
|
|
171
|
+
def db_disconnect!(class_name)
|
|
172
|
+
with_class class_name do |clazz|
|
|
173
|
+
clazz.connection.disconnect!
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# hides LocalIndex#rebuild_index to implement index versioning
|
|
178
|
+
def rebuild_index(index_name)
|
|
179
|
+
definition = ActsAsFerret::get_index(index_name).index_definition.dup
|
|
180
|
+
models = definition[:registered_models]
|
|
181
|
+
index = new_index_for(definition)
|
|
182
|
+
# TODO fix reconnection stuff
|
|
183
|
+
# reconnect_when_needed(clazz) do
|
|
184
|
+
# @logger.debug "DRb server: rebuild index for class(es) #{models.inspect} in #{index.options[:path]}"
|
|
185
|
+
index.index_models models
|
|
186
|
+
# end
|
|
187
|
+
new_version = File.join definition[:index_base_dir], Time.now.utc.strftime('%Y%m%d%H%M%S')
|
|
188
|
+
# create a unique directory name (needed for unit tests where
|
|
189
|
+
# multiple rebuilds per second may occur)
|
|
190
|
+
if File.exists?(new_version)
|
|
191
|
+
i = 0
|
|
192
|
+
i+=1 while File.exists?("#{new_version}_#{i}")
|
|
193
|
+
new_version << "_#{i}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
File.rename index.options[:path], new_version
|
|
197
|
+
ActsAsFerret::change_index_dir index_name, new_version
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
protected
|
|
202
|
+
|
|
203
|
+
def reconnect_when_needed(clazz)
|
|
204
|
+
retried = false
|
|
205
|
+
begin
|
|
206
|
+
yield
|
|
207
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
208
|
+
if e.message =~ /MySQL server has gone away/
|
|
209
|
+
if retried
|
|
210
|
+
raise e
|
|
211
|
+
else
|
|
212
|
+
@logger.info "StatementInvalid caught, trying to reconnect..."
|
|
213
|
+
clazz.connection.reconnect!
|
|
214
|
+
retried = true
|
|
215
|
+
retry
|
|
216
|
+
end
|
|
217
|
+
else
|
|
218
|
+
@logger.error "StatementInvalid caught, but unsure what to do with it: #{e}"
|
|
219
|
+
raise e
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def new_index_for(index_definition)
|
|
225
|
+
ferret_cfg = index_definition[:ferret].dup
|
|
226
|
+
ferret_cfg.update :auto_flush => false,
|
|
227
|
+
:create => true,
|
|
228
|
+
:field_infos => ActsAsFerret::field_infos(index_definition),
|
|
229
|
+
:path => File.join(index_definition[:index_base_dir], 'rebuild')
|
|
230
|
+
returning Ferret::Index::Index.new(ferret_cfg) do |i|
|
|
231
|
+
i.batch_size = index_definition[:reindex_batch_size]
|
|
232
|
+
i.logger = @logger
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
data/lib/index.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module ActsAsFerret
|
|
2
|
+
|
|
3
|
+
class IndexLogger
|
|
4
|
+
def initialize(logger, name)
|
|
5
|
+
@logger = logger
|
|
6
|
+
@index_name = name
|
|
7
|
+
end
|
|
8
|
+
%w(debug info warn error).each do |m|
|
|
9
|
+
define_method(m) do |message|
|
|
10
|
+
@logger.send m, "[#{@index_name}] #{message}"
|
|
11
|
+
end
|
|
12
|
+
question = :"#{m}?"
|
|
13
|
+
define_method(question) do
|
|
14
|
+
@logger.send question
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# base class for local and remote indexes
|
|
20
|
+
class AbstractIndex
|
|
21
|
+
include FerretFindMethods
|
|
22
|
+
|
|
23
|
+
attr_reader :logger, :index_name, :index_definition, :registered_models_config
|
|
24
|
+
def initialize(index_definition)
|
|
25
|
+
@index_definition = index_definition
|
|
26
|
+
@registered_models_config = {}
|
|
27
|
+
@index_name = index_definition[:name]
|
|
28
|
+
@logger = IndexLogger.new(ActsAsFerret::logger, @index_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# TODO allow for per-class field configuration (i.e. different via, boosts
|
|
32
|
+
# for the same field among different models)
|
|
33
|
+
def register_class(clazz, options = {})
|
|
34
|
+
logger.info "register class #{clazz} with index #{index_name}"
|
|
35
|
+
|
|
36
|
+
if force = options.delete(:force_re_registration)
|
|
37
|
+
index_definition[:registered_models].delete(clazz)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
if index_definition[:registered_models].map(&:name).include?(clazz.name)
|
|
41
|
+
logger.info("refusing re-registration of class #{clazz}")
|
|
42
|
+
else
|
|
43
|
+
index_definition[:registered_models] << clazz
|
|
44
|
+
@registered_models_config[clazz] = options
|
|
45
|
+
|
|
46
|
+
# merge fields from this acts_as_ferret call with predefined fields
|
|
47
|
+
already_defined_fields = index_definition[:ferret_fields]
|
|
48
|
+
field_config = ActsAsFerret::build_field_config options[:fields]
|
|
49
|
+
field_config.update ActsAsFerret::build_field_config( options[:additional_fields] )
|
|
50
|
+
field_config.each do |field, config|
|
|
51
|
+
if already_defined_fields.has_key?(field)
|
|
52
|
+
logger.info "ignoring redefinition of ferret field #{field}" if shared?
|
|
53
|
+
else
|
|
54
|
+
already_defined_fields[field] = config
|
|
55
|
+
logger.info "adding new field #{field} from class #{clazz.name} to index #{index_name}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# update default field list to be used by the query parser, unless it
|
|
60
|
+
# was explicitly given by user.
|
|
61
|
+
#
|
|
62
|
+
# It will include all content fields *not* marked as :untokenized.
|
|
63
|
+
# This fixes the otherwise failing CommentTest#test_stopwords. Basically
|
|
64
|
+
# this means that by default only tokenized fields (which all fields are
|
|
65
|
+
# by default) will be searched. If you want to search inside the contents
|
|
66
|
+
# of an untokenized field, you'll have to explicitly specify it in your
|
|
67
|
+
# query.
|
|
68
|
+
unless index_definition[:user_default_field]
|
|
69
|
+
# grab all tokenized fields
|
|
70
|
+
ferret_fields = index_definition[:ferret_fields]
|
|
71
|
+
index_definition[:ferret][:default_field] = ferret_fields.keys.select do |field|
|
|
72
|
+
ferret_fields[field][:index] != :untokenized
|
|
73
|
+
end
|
|
74
|
+
logger.info "default field list for index #{index_name}: #{index_definition[:ferret][:default_field].inspect}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return index_definition
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# true if this index is used by more than one model class
|
|
82
|
+
def shared?
|
|
83
|
+
index_definition[:registered_models].size > 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Switches the index to a new index directory.
|
|
87
|
+
# Used by the DRb server when switching to a new index version.
|
|
88
|
+
def change_index_dir(new_dir)
|
|
89
|
+
logger.debug "[#{index_name}] changing index dir to #{new_dir}"
|
|
90
|
+
index_definition[:index_dir] = index_definition[:ferret][:path] = new_dir
|
|
91
|
+
reopen!
|
|
92
|
+
logger.debug "[#{index_name}] index dir is now #{new_dir}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
protected
|
|
96
|
+
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|