DrMark-thinking-sphinx 0.9.9 → 1.1.6

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 (77) hide show
  1. data/README +64 -2
  2. data/lib/thinking_sphinx.rb +88 -11
  3. data/lib/thinking_sphinx/active_record.rb +136 -21
  4. data/lib/thinking_sphinx/active_record/delta.rb +43 -62
  5. data/lib/thinking_sphinx/active_record/has_many_association.rb +1 -1
  6. data/lib/thinking_sphinx/active_record/search.rb +7 -0
  7. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  8. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  9. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +130 -0
  10. data/lib/thinking_sphinx/association.rb +17 -0
  11. data/lib/thinking_sphinx/attribute.rb +171 -97
  12. data/lib/thinking_sphinx/collection.rb +126 -2
  13. data/lib/thinking_sphinx/configuration.rb +120 -171
  14. data/lib/thinking_sphinx/core/string.rb +15 -0
  15. data/lib/thinking_sphinx/deltas.rb +27 -0
  16. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  17. data/lib/thinking_sphinx/deltas/default_delta.rb +67 -0
  18. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  19. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  20. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  22. data/lib/thinking_sphinx/facet.rb +58 -0
  23. data/lib/thinking_sphinx/facet_collection.rb +60 -0
  24. data/lib/thinking_sphinx/field.rb +18 -52
  25. data/lib/thinking_sphinx/index.rb +246 -199
  26. data/lib/thinking_sphinx/index/builder.rb +85 -16
  27. data/lib/thinking_sphinx/rails_additions.rb +85 -5
  28. data/lib/thinking_sphinx/search.rb +459 -190
  29. data/lib/thinking_sphinx/tasks.rb +128 -0
  30. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +53 -124
  31. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +2 -2
  32. data/spec/unit/thinking_sphinx/active_record_spec.rb +110 -30
  33. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -149
  34. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  35. data/spec/unit/thinking_sphinx/configuration_spec.rb +54 -412
  36. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  37. data/spec/unit/thinking_sphinx/field_spec.rb +0 -79
  38. data/spec/unit/thinking_sphinx/index/builder_spec.rb +1 -29
  39. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +1 -39
  40. data/spec/unit/thinking_sphinx/index_spec.rb +78 -226
  41. data/spec/unit/thinking_sphinx/search_spec.rb +29 -228
  42. data/spec/unit/thinking_sphinx_spec.rb +23 -19
  43. data/tasks/distribution.rb +48 -0
  44. data/tasks/rails.rake +1 -0
  45. data/tasks/testing.rb +86 -0
  46. data/vendor/after_commit/LICENSE +20 -0
  47. data/vendor/after_commit/README +16 -0
  48. data/vendor/after_commit/Rakefile +22 -0
  49. data/vendor/after_commit/init.rb +8 -0
  50. data/vendor/after_commit/lib/after_commit.rb +45 -0
  51. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  52. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  53. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  54. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  55. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  56. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  57. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  58. data/{lib → vendor/riddle/lib}/riddle.rb +9 -5
  59. data/{lib → vendor/riddle/lib}/riddle/client.rb +6 -26
  60. data/{lib → vendor/riddle/lib}/riddle/client/filter.rb +10 -1
  61. data/{lib → vendor/riddle/lib}/riddle/client/message.rb +0 -0
  62. data/{lib → vendor/riddle/lib}/riddle/client/response.rb +0 -0
  63. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  64. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  65. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  66. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  67. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  68. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  69. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  70. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  71. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  72. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  73. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  74. metadata +63 -10
  75. data/lib/test.rb +0 -46
  76. data/tasks/thinking_sphinx_tasks.rake +0 -1
  77. data/tasks/thinking_sphinx_tasks.rb +0 -86
data/README CHANGED
@@ -10,28 +10,54 @@ Keep in mind that while Thinking Sphinx works for ActiveRecord with Merb, it doe
10
10
 
11
11
  Fork on GitHub and after you've committed tested patches, send a pull request.
12
12
 
13
+ To quickly see if your system is ready to run the thinking sphinx specs, run the contribute.rb script found in the project root directory. Use the following instructions to install any missing requirements.
14
+
13
15
  To get the spec suite running, you will need to install the not-a-mock gem if you don't already have it:
14
16
 
15
17
  git clone git://github.com/freelancing-god/not-a-mock.git
16
18
  cd not-a-mock
17
19
  rake gem
18
20
  gem install pkg/not_a_mock-1.1.0.gem
21
+
22
+ Then install the ginger gem. The steps are the same, except that you might need to sudo the gem install:
23
+
24
+ git clone git://github.com/freelancing-god/ginger.git
25
+ cd ginger
26
+ rake gem
27
+ sudo gem install pkg/ginger-1.1.0.gem
19
28
 
20
- Then set up your database
29
+ Then install the faker gem:
30
+
31
+ sudo gem install faker
32
+
33
+ Then set up your database:
21
34
 
22
35
  cp spec/fixtures/database.yml.default spec/fixtures/database.yml
36
+ cp spec/fixtures/database.yml.default features/support/db/database.yml
23
37
  mysqladmin -u root create thinking_sphinx
38
+
39
+ Make sure you don't have another Sphinx daemon (searchd) running. If you do, quit it with "rake ts:stop"
40
+ in the app root.
24
41
 
25
42
  You should now have a passing test suite from which to build your patch on.
26
43
 
27
44
  rake spec
45
+ rake features (Note: You will need MySQL and Postgres gems to run the full suite. You may want to tweak your rakefile)
46
+
47
+ If you get the message "Failed to start searchd daemon", run the spec with sudo:
48
+
49
+ sudo rake spec
50
+ sudo rake features
51
+
52
+ If you quit the spec suite before it's completed, you may be left with data in the test
53
+ database, causing the next run to have failures. Let that run complete and then try again.
28
54
 
29
55
  == Contributors
30
56
 
31
57
  Since I first released this library, there's been quite a few people who have submitted patches, to my immense gratitude. Others have suggested syntax changes and general improvements. So my thanks to the following people:
32
58
 
33
59
  - Joost Hietbrink
34
- - Jonathon Conway
60
+ - Jonathan Conway
35
61
  - Gregory Mirzayantz
36
62
  - Tung Nguyen
37
63
  - Sean Cribbs
@@ -63,3 +89,39 @@ Since I first released this library, there's been quite a few people who have su
63
89
  - David Eisinger
64
90
  - Shay Arnett
65
91
  - Minh Tran
92
+ - Jeremy Durham
93
+ - Piotr Sarnacki
94
+ - Matt Johnson
95
+ - Nicolas Blanco
96
+ - Max Lapshin
97
+ - Josh Natanson
98
+ - Philip Hallstrom
99
+ - Christian Rishøj
100
+ - Mike Flester
101
+ - Jim Remsik
102
+ - Kennon Ballou
103
+ - Henrik Nyh
104
+ - Emil Tin
105
+ - Doug Cole
106
+ - Ed Hickey
107
+ - Evan Weaver
108
+ - Thibaut Barrere
109
+ - Kristopher Chambers
110
+ - Dmitrij Smalko
111
+ - Aleksey Yeschenko
112
+ - Lachie Cox
113
+ - Lourens Naude
114
+ - Tom Davies
115
+ - Dan Pickett
116
+ - Alex Caudill
117
+ - Jim Benton
118
+ - John Aughey
119
+ - Keith Pitty
120
+ - Jeff Talbot
121
+ - Dana Contreras
122
+ - Menno van der Sman
123
+ - Bill Harding
124
+ - Isaac Feliu
125
+ - Andrei Bocan
126
+ - László Bácsi
127
+ - Peter Wagenet
@@ -1,27 +1,41 @@
1
+ Dir[File.join(File.dirname(__FILE__), '../vendor/*/lib')].each do |path|
2
+ $LOAD_PATH.unshift path
3
+ end
4
+
1
5
  require 'active_record'
2
6
  require 'riddle'
7
+ require 'after_commit'
3
8
 
9
+ require 'thinking_sphinx/core/string'
4
10
  require 'thinking_sphinx/active_record'
5
11
  require 'thinking_sphinx/association'
6
12
  require 'thinking_sphinx/attribute'
7
13
  require 'thinking_sphinx/collection'
8
14
  require 'thinking_sphinx/configuration'
15
+ require 'thinking_sphinx/facet'
16
+ require 'thinking_sphinx/class_facet'
17
+ require 'thinking_sphinx/facet_collection'
9
18
  require 'thinking_sphinx/field'
10
19
  require 'thinking_sphinx/index'
11
20
  require 'thinking_sphinx/rails_additions'
12
21
  require 'thinking_sphinx/search'
22
+ require 'thinking_sphinx/deltas'
23
+
24
+ require 'thinking_sphinx/adapters/abstract_adapter'
25
+ require 'thinking_sphinx/adapters/mysql_adapter'
26
+ require 'thinking_sphinx/adapters/postgresql_adapter'
13
27
 
14
28
  ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
15
29
 
16
30
  Merb::Plugins.add_rakefiles(
17
- File.join(File.dirname(__FILE__), "..", "tasks", "thinking_sphinx_tasks")
31
+ File.join(File.dirname(__FILE__), "thinking_sphinx", "tasks")
18
32
  ) if defined?(Merb)
19
33
 
20
34
  module ThinkingSphinx
21
35
  module Version #:nodoc:
22
- Major = 0
23
- Minor = 9
24
- Tiny = 9
36
+ Major = 1
37
+ Minor = 1
38
+ Tiny = 6
25
39
 
26
40
  String = [Major, Minor, Tiny].join('.')
27
41
  end
@@ -31,6 +45,15 @@ module ThinkingSphinx
31
45
  class ConnectionError < StandardError
32
46
  end
33
47
 
48
+ # A StaleIdsException is thrown by Collection.instances_from_matches if there
49
+ # are records in Sphinx but not in the database, so the search can be retried.
50
+ class StaleIdsException < StandardError
51
+ attr_accessor :ids
52
+ def initialize(ids)
53
+ self.ids = ids
54
+ end
55
+ end
56
+
34
57
  # The collection of indexed models. Keep in mind that Rails lazily loads
35
58
  # its classes, so this may not actually be populated with _all_ the models
36
59
  # that have Sphinx indexes.
@@ -38,6 +61,10 @@ module ThinkingSphinx
38
61
  @@indexed_models ||= []
39
62
  end
40
63
 
64
+ def self.unique_id_expression(offset = nil)
65
+ "* #{ThinkingSphinx.indexed_models.size} + #{offset || 0}"
66
+ end
67
+
41
68
  # Check if index definition is disabled.
42
69
  #
43
70
  def self.define_indexes?
@@ -88,16 +115,66 @@ module ThinkingSphinx
88
115
  @@updates_enabled = value
89
116
  end
90
117
 
118
+ @@suppress_delta_output = false
119
+
120
+ def self.suppress_delta_output?
121
+ @@suppress_delta_output
122
+ end
123
+
124
+ def self.suppress_delta_output=(value)
125
+ @@suppress_delta_output = value
126
+ end
127
+
91
128
  # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
92
129
  # or if not using MySQL, this will return false.
93
130
  #
94
131
  def self.use_group_by_shortcut?
95
- ::ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") &&
96
- ::ActiveRecord::Base.connection.is_a?(
97
- ::ActiveRecord::ConnectionAdapters::MysqlAdapter
98
- ) &&
99
- ::ActiveRecord::Base.connection.select_all(
100
- "SELECT @@global.sql_mode, @@session.sql_mode;"
101
- ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
132
+ !!(
133
+ mysql? && ::ActiveRecord::Base.connection.select_all(
134
+ "SELECT @@global.sql_mode, @@session.sql_mode;"
135
+ ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
136
+ )
137
+ end
138
+
139
+ def self.sphinx_running?
140
+ !!sphinx_pid && pid_active?(sphinx_pid)
141
+ end
142
+
143
+ def self.sphinx_pid
144
+ pid_file = ThinkingSphinx::Configuration.instance.pid_file
145
+ cat_command = 'cat'
146
+ return nil unless File.exists?(pid_file)
147
+
148
+ if microsoft?
149
+ pid_file.gsub!('/', '\\')
150
+ cat_command = 'type'
151
+ end
152
+
153
+ `#{cat_command} #{pid_file}`[/\d+/]
154
+ end
155
+
156
+ def self.pid_active?(pid)
157
+ return true if microsoft?
158
+
159
+ begin
160
+ # In JRuby this returns -1 if the process doesn't exist
161
+ Process.getpgid(pid.to_i) != -1
162
+ rescue Exception => e
163
+ false
164
+ end
165
+ end
166
+
167
+ def self.microsoft?
168
+ RUBY_PLATFORM =~ /mswin/
169
+ end
170
+
171
+ def self.jruby?
172
+ defined?(JRUBY_VERSION)
173
+ end
174
+
175
+ def self.mysql?
176
+ ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlAdapter" || (
177
+ jruby? && ::ActiveRecord::Base.connection.config[:adapter] == "jdbcmysql"
178
+ )
102
179
  end
103
180
  end
@@ -5,12 +5,12 @@ require 'thinking_sphinx/active_record/has_many_association'
5
5
  module ThinkingSphinx
6
6
  # Core additions to ActiveRecord models - define_index for creating indexes
7
7
  # for models. If you want to interrogate the index objects created for the
8
- # model, you can use the class-level accessor :indexes.
8
+ # model, you can use the class-level accessor :sphinx_indexes.
9
9
  #
10
10
  module ActiveRecord
11
11
  def self.included(base)
12
12
  base.class_eval do
13
- class_inheritable_array :indexes
13
+ class_inheritable_array :sphinx_indexes, :sphinx_facets
14
14
  class << self
15
15
  # Allows creation of indexes for Sphinx. If you don't do this, there
16
16
  # isn't much point trying to search (or using this plugin at all,
@@ -64,10 +64,10 @@ module ThinkingSphinx
64
64
  def define_index(&block)
65
65
  return unless ThinkingSphinx.define_indexes?
66
66
 
67
- self.indexes ||= []
67
+ self.sphinx_indexes ||= []
68
68
  index = Index.new(self, &block)
69
69
 
70
- self.indexes << index
70
+ self.sphinx_indexes << index
71
71
  unless ThinkingSphinx.indexed_models.include?(self.name)
72
72
  ThinkingSphinx.indexed_models << self.name
73
73
  end
@@ -83,6 +83,10 @@ module ThinkingSphinx
83
83
  end
84
84
  alias_method :sphinx_index, :define_index
85
85
 
86
+ def sphinx_index_options
87
+ sphinx_indexes.last.options
88
+ end
89
+
86
90
  # Generate a unique CRC value for the model's name, to use to
87
91
  # determine which Sphinx documents belong to which AR records.
88
92
  #
@@ -90,19 +94,105 @@ module ThinkingSphinx
90
94
  # you in some other way, awesome.
91
95
  #
92
96
  def to_crc32
93
- result = 0xFFFFFFFF
94
- self.name.each_byte do |byte|
95
- result ^= byte
96
- 8.times do
97
- result = (result >> 1) ^ (0xEDB88320 * (result & 1))
98
- end
99
- end
100
- result ^ 0xFFFFFFFF
97
+ self.name.to_crc32
101
98
  end
102
99
 
103
100
  def to_crc32s
104
101
  (subclasses << self).collect { |klass| klass.to_crc32 }
105
102
  end
103
+
104
+ def source_of_sphinx_index
105
+ possible_models = self.sphinx_indexes.collect { |index| index.model }
106
+ return self if possible_models.include?(self)
107
+
108
+ parent = self.superclass
109
+ while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
110
+ parent = parent.superclass
111
+ end
112
+
113
+ return parent
114
+ end
115
+
116
+ def to_riddle(offset)
117
+ sphinx_database_adapter.setup
118
+
119
+ indexes = [to_riddle_for_core(offset)]
120
+ indexes << to_riddle_for_delta(offset) if sphinx_delta?
121
+ indexes << to_riddle_for_distributed
122
+ end
123
+
124
+ def sphinx_database_adapter
125
+ @sphinx_database_adapter ||=
126
+ ThinkingSphinx::AbstractAdapter.detect(self)
127
+ end
128
+
129
+ private
130
+
131
+ def sphinx_name
132
+ self.name.underscore.tr(':/\\', '_')
133
+ end
134
+
135
+ def sphinx_delta?
136
+ self.sphinx_indexes.any? { |index| index.delta? }
137
+ end
138
+
139
+ def to_riddle_for_core(offset)
140
+ index = Riddle::Configuration::Index.new("#{sphinx_name}_core")
141
+ index.path = File.join(
142
+ ThinkingSphinx::Configuration.instance.searchd_file_path, index.name
143
+ )
144
+
145
+ set_configuration_options_for_indexes index
146
+ set_field_settings_for_indexes index
147
+
148
+ self.sphinx_indexes.select { |ts_index|
149
+ ts_index.model == self
150
+ }.each_with_index do |ts_index, i|
151
+ index.sources << ts_index.to_riddle_for_core(offset, i)
152
+ end
153
+
154
+ index
155
+ end
156
+
157
+ def to_riddle_for_delta(offset)
158
+ index = Riddle::Configuration::Index.new("#{sphinx_name}_delta")
159
+ index.parent = "#{sphinx_name}_core"
160
+ index.path = File.join(ThinkingSphinx::Configuration.instance.searchd_file_path, index.name)
161
+
162
+ self.sphinx_indexes.each_with_index do |ts_index, i|
163
+ index.sources << ts_index.to_riddle_for_delta(offset, i) if ts_index.delta?
164
+ end
165
+
166
+ index
167
+ end
168
+
169
+ def to_riddle_for_distributed
170
+ index = Riddle::Configuration::DistributedIndex.new(sphinx_name)
171
+ index.local_indexes << "#{sphinx_name}_core"
172
+ index.local_indexes.unshift "#{sphinx_name}_delta" if sphinx_delta?
173
+ index
174
+ end
175
+
176
+ def set_configuration_options_for_indexes(index)
177
+ ThinkingSphinx::Configuration.instance.index_options.each do |key, value|
178
+ index.send("#{key}=".to_sym, value)
179
+ end
180
+
181
+ self.sphinx_indexes.each do |ts_index|
182
+ ts_index.options.each do |key, value|
183
+ index.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) && !value.nil?
184
+ end
185
+ end
186
+ end
187
+
188
+ def set_field_settings_for_indexes(index)
189
+ field_names = lambda { |field| field.unique_name.to_s }
190
+
191
+ self.sphinx_indexes.each do |ts_index|
192
+ index.prefix_field_names += ts_index.prefix_fields.collect(&field_names)
193
+ index.infix_field_names += ts_index.infix_fields.collect(&field_names)
194
+ end
195
+ end
106
196
  end
107
197
  end
108
198
 
@@ -117,29 +207,54 @@ module ThinkingSphinx
117
207
  )
118
208
  end
119
209
 
210
+ def in_index?(suffix)
211
+ self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
212
+ end
213
+
120
214
  def in_core_index?
121
- @in_core_index ||= self.class.search_for_id(self.id, "#{self.class.name.downcase}_core")
215
+ in_index? "core"
216
+ end
217
+
218
+ def in_delta_index?
219
+ in_index? "delta"
220
+ end
221
+
222
+ def in_both_indexes?
223
+ in_core_index? && in_delta_index?
122
224
  end
123
225
 
124
226
  def toggle_deleted
125
- return unless ThinkingSphinx.updates_enabled?
227
+ return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running?
126
228
 
127
- config = ThinkingSphinx::Configuration.new
229
+ config = ThinkingSphinx::Configuration.instance
128
230
  client = Riddle::Client.new config.address, config.port
129
231
 
130
232
  client.update(
131
- "#{self.class.indexes.first.name}_core",
233
+ "#{self.class.sphinx_indexes.first.name}_core",
132
234
  ['sphinx_deleted'],
133
- {self.id => 1}
235
+ {self.sphinx_document_id => 1}
134
236
  ) if self.in_core_index?
135
237
 
136
238
  client.update(
137
- "#{self.class.indexes.first.name}_delta",
239
+ "#{self.class.sphinx_indexes.first.name}_delta",
138
240
  ['sphinx_deleted'],
139
- {self.id => 1}
241
+ {self.sphinx_document_id => 1}
140
242
  ) if ThinkingSphinx.deltas_enabled? &&
141
- self.class.indexes.any? { |index| index.delta? } &&
142
- self.delta?
243
+ self.class.sphinx_indexes.any? { |index| index.delta? } &&
244
+ self.toggled_delta?
245
+ rescue ::ThinkingSphinx::ConnectionError
246
+ # nothing
247
+ end
248
+
249
+ def sphinx_document_id
250
+ (self.id * ThinkingSphinx.indexed_models.size) +
251
+ ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name)
252
+ end
253
+
254
+ private
255
+
256
+ def sphinx_index_name(suffix)
257
+ "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
143
258
  end
144
259
  end
145
260
  end