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.
Files changed (162) hide show
  1. data/LICENSE +20 -0
  2. data/README +68 -0
  3. data/bin/aaf_install +23 -0
  4. data/config/ferret_server.yml +24 -0
  5. data/doc/README.win32 +23 -0
  6. data/doc/demo/README +154 -0
  7. data/doc/demo/README_DEMO +23 -0
  8. data/doc/demo/Rakefile +10 -0
  9. data/doc/demo/app/controllers/admin/backend_controller.rb +14 -0
  10. data/doc/demo/app/controllers/admin_area_controller.rb +4 -0
  11. data/doc/demo/app/controllers/application.rb +5 -0
  12. data/doc/demo/app/controllers/contents_controller.rb +49 -0
  13. data/doc/demo/app/controllers/searches_controller.rb +8 -0
  14. data/doc/demo/app/helpers/admin/backend_helper.rb +2 -0
  15. data/doc/demo/app/helpers/application_helper.rb +3 -0
  16. data/doc/demo/app/helpers/content_helper.rb +2 -0
  17. data/doc/demo/app/helpers/search_helper.rb +2 -0
  18. data/doc/demo/app/models/comment.rb +48 -0
  19. data/doc/demo/app/models/content.rb +12 -0
  20. data/doc/demo/app/models/content_base.rb +28 -0
  21. data/doc/demo/app/models/search.rb +19 -0
  22. data/doc/demo/app/models/shared_index1.rb +3 -0
  23. data/doc/demo/app/models/shared_index2.rb +3 -0
  24. data/doc/demo/app/models/special_content.rb +3 -0
  25. data/doc/demo/app/models/stats.rb +20 -0
  26. data/doc/demo/app/views/admin/backend/search.rhtml +18 -0
  27. data/doc/demo/app/views/contents/_form.rhtml +10 -0
  28. data/doc/demo/app/views/contents/edit.rhtml +9 -0
  29. data/doc/demo/app/views/contents/index.rhtml +24 -0
  30. data/doc/demo/app/views/contents/new.rhtml +8 -0
  31. data/doc/demo/app/views/contents/show.rhtml +8 -0
  32. data/doc/demo/app/views/layouts/application.html.erb +17 -0
  33. data/doc/demo/app/views/searches/_content.html.erb +2 -0
  34. data/doc/demo/app/views/searches/search.html.erb +20 -0
  35. data/doc/demo/config/boot.rb +109 -0
  36. data/doc/demo/config/database.yml +38 -0
  37. data/doc/demo/config/environment.rb +69 -0
  38. data/doc/demo/config/environments/development.rb +16 -0
  39. data/doc/demo/config/environments/production.rb +19 -0
  40. data/doc/demo/config/environments/test.rb +21 -0
  41. data/doc/demo/config/ferret_server.yml +18 -0
  42. data/doc/demo/config/lighttpd.conf +40 -0
  43. data/doc/demo/config/routes.rb +9 -0
  44. data/doc/demo/db/development_structure.sql +15 -0
  45. data/doc/demo/db/migrate/001_initial_migration.rb +18 -0
  46. data/doc/demo/db/migrate/002_add_type_to_contents.rb +9 -0
  47. data/doc/demo/db/migrate/003_create_shared_index1s.rb +11 -0
  48. data/doc/demo/db/migrate/004_create_shared_index2s.rb +11 -0
  49. data/doc/demo/db/migrate/005_special_field.rb +9 -0
  50. data/doc/demo/db/migrate/006_create_stats.rb +15 -0
  51. data/doc/demo/db/schema.sql +18 -0
  52. data/doc/demo/doc/README_FOR_APP +2 -0
  53. data/doc/demo/doc/howto.txt +70 -0
  54. data/doc/demo/public/.htaccess +40 -0
  55. data/doc/demo/public/404.html +8 -0
  56. data/doc/demo/public/500.html +8 -0
  57. data/doc/demo/public/dispatch.cgi +10 -0
  58. data/doc/demo/public/dispatch.fcgi +24 -0
  59. data/doc/demo/public/dispatch.rb +10 -0
  60. data/doc/demo/public/favicon.ico +0 -0
  61. data/doc/demo/public/images/rails.png +0 -0
  62. data/doc/demo/public/index.html +277 -0
  63. data/doc/demo/public/robots.txt +1 -0
  64. data/doc/demo/public/stylesheets/scaffold.css +74 -0
  65. data/doc/demo/script/about +3 -0
  66. data/doc/demo/script/breakpointer +3 -0
  67. data/doc/demo/script/console +3 -0
  68. data/doc/demo/script/destroy +3 -0
  69. data/doc/demo/script/ferret_server +10 -0
  70. data/doc/demo/script/generate +3 -0
  71. data/doc/demo/script/performance/benchmarker +3 -0
  72. data/doc/demo/script/performance/profiler +3 -0
  73. data/doc/demo/script/plugin +3 -0
  74. data/doc/demo/script/process/inspector +3 -0
  75. data/doc/demo/script/process/reaper +3 -0
  76. data/doc/demo/script/process/spawner +3 -0
  77. data/doc/demo/script/process/spinner +3 -0
  78. data/doc/demo/script/runner +3 -0
  79. data/doc/demo/script/server +3 -0
  80. data/doc/demo/test/fixtures/comments.yml +12 -0
  81. data/doc/demo/test/fixtures/contents.yml +13 -0
  82. data/doc/demo/test/fixtures/remote_contents.yml +9 -0
  83. data/doc/demo/test/fixtures/shared_index1s.yml +7 -0
  84. data/doc/demo/test/fixtures/shared_index2s.yml +7 -0
  85. data/doc/demo/test/functional/admin/backend_controller_test.rb +35 -0
  86. data/doc/demo/test/functional/contents_controller_test.rb +81 -0
  87. data/doc/demo/test/functional/searches_controller_test.rb +71 -0
  88. data/doc/demo/test/smoke/drb_smoke_test.rb +321 -0
  89. data/doc/demo/test/smoke/process_stats.rb +21 -0
  90. data/doc/demo/test/test_helper.rb +30 -0
  91. data/doc/demo/test/unit/comment_test.rb +217 -0
  92. data/doc/demo/test/unit/content_test.rb +705 -0
  93. data/doc/demo/test/unit/ferret_result_test.rb +24 -0
  94. data/doc/demo/test/unit/multi_index_test.rb +329 -0
  95. data/doc/demo/test/unit/remote_index_test.rb +23 -0
  96. data/doc/demo/test/unit/shared_index1_test.rb +108 -0
  97. data/doc/demo/test/unit/shared_index2_test.rb +13 -0
  98. data/doc/demo/test/unit/sort_test.rb +21 -0
  99. data/doc/demo/test/unit/special_content_test.rb +25 -0
  100. data/doc/demo/vendor/plugins/will_paginate/LICENSE +18 -0
  101. data/doc/demo/vendor/plugins/will_paginate/README +108 -0
  102. data/doc/demo/vendor/plugins/will_paginate/Rakefile +23 -0
  103. data/doc/demo/vendor/plugins/will_paginate/init.rb +21 -0
  104. data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/collection.rb +45 -0
  105. data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb +44 -0
  106. data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/finder.rb +159 -0
  107. data/doc/demo/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb +95 -0
  108. data/doc/demo/vendor/plugins/will_paginate/test/array_pagination_test.rb +23 -0
  109. data/doc/demo/vendor/plugins/will_paginate/test/boot.rb +27 -0
  110. data/doc/demo/vendor/plugins/will_paginate/test/console +10 -0
  111. data/doc/demo/vendor/plugins/will_paginate/test/finder_test.rb +219 -0
  112. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/admin.rb +3 -0
  113. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/companies.yml +24 -0
  114. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/company.rb +23 -0
  115. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/developer.rb +11 -0
  116. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml +13 -0
  117. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/project.rb +4 -0
  118. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/projects.yml +7 -0
  119. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/replies.yml +20 -0
  120. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/reply.rb +5 -0
  121. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/schema.sql +44 -0
  122. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/topic.rb +19 -0
  123. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/topics.yml +30 -0
  124. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/user.rb +2 -0
  125. data/doc/demo/vendor/plugins/will_paginate/test/fixtures/users.yml +35 -0
  126. data/doc/demo/vendor/plugins/will_paginate/test/helper.rb +42 -0
  127. data/doc/demo/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb +64 -0
  128. data/doc/demo/vendor/plugins/will_paginate/test/lib/load_fixtures.rb +10 -0
  129. data/doc/demo/vendor/plugins/will_paginate/test/pagination_test.rb +136 -0
  130. data/doc/monit-example +22 -0
  131. data/init.rb +24 -0
  132. data/install.rb +18 -0
  133. data/lib/act_methods.rb +147 -0
  134. data/lib/acts_as_ferret.rb +584 -0
  135. data/lib/ar_mysql_auto_reconnect_patch.rb +41 -0
  136. data/lib/blank_slate.rb +53 -0
  137. data/lib/bulk_indexer.rb +38 -0
  138. data/lib/class_methods.rb +270 -0
  139. data/lib/ferret_extensions.rb +188 -0
  140. data/lib/ferret_find_methods.rb +141 -0
  141. data/lib/ferret_result.rb +53 -0
  142. data/lib/ferret_server.rb +238 -0
  143. data/lib/index.rb +99 -0
  144. data/lib/instance_methods.rb +171 -0
  145. data/lib/local_index.rb +205 -0
  146. data/lib/more_like_this.rb +217 -0
  147. data/lib/multi_index.rb +126 -0
  148. data/lib/rdig_adapter.rb +148 -0
  149. data/lib/remote_functions.rb +23 -0
  150. data/lib/remote_index.rb +54 -0
  151. data/lib/remote_multi_index.rb +20 -0
  152. data/lib/search_results.rb +50 -0
  153. data/lib/server_manager.rb +58 -0
  154. data/lib/unix_daemon.rb +64 -0
  155. data/lib/without_ar.rb +52 -0
  156. data/rakefile +141 -0
  157. data/recipes/aaf_recipes.rb +114 -0
  158. data/script/ferret_daemon +94 -0
  159. data/script/ferret_server +10 -0
  160. data/script/ferret_service +178 -0
  161. data/tasks/ferret.rake +22 -0
  162. 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