thinking-sphinx 1.2.13 → 1.4.0

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 (204) hide show
  1. data/README.textile +37 -4
  2. data/VERSION +1 -0
  3. data/features/abstract_inheritance.feature +10 -0
  4. data/features/alternate_primary_key.feature +1 -1
  5. data/features/attribute_updates.feature +49 -5
  6. data/features/deleting_instances.feature +3 -0
  7. data/features/excerpts.feature +8 -0
  8. data/features/facets.feature +15 -1
  9. data/features/facets_across_model.feature +2 -2
  10. data/features/field_sorting.feature +18 -0
  11. data/features/handling_edits.feature +1 -1
  12. data/features/searching_across_models.feature +2 -2
  13. data/features/searching_by_index.feature +40 -0
  14. data/features/searching_by_model.feature +1 -8
  15. data/features/sphinx_scopes.feature +33 -0
  16. data/features/step_definitions/alpha_steps.rb +14 -1
  17. data/features/step_definitions/beta_steps.rb +1 -1
  18. data/features/step_definitions/common_steps.rb +21 -2
  19. data/features/step_definitions/facet_steps.rb +4 -0
  20. data/features/step_definitions/scope_steps.rb +8 -0
  21. data/features/step_definitions/search_steps.rb +5 -0
  22. data/features/step_definitions/sphinx_steps.rb +8 -4
  23. data/features/sti_searching.feature +5 -0
  24. data/features/support/env.rb +7 -6
  25. data/features/{support → thinking_sphinx}/db/fixtures/betas.rb +1 -0
  26. data/features/{support → thinking_sphinx}/db/fixtures/comments.rb +1 -1
  27. data/features/{support → thinking_sphinx}/db/fixtures/developers.rb +2 -0
  28. data/features/thinking_sphinx/db/fixtures/foxes.rb +3 -0
  29. data/features/thinking_sphinx/db/fixtures/music.rb +4 -0
  30. data/features/{support → thinking_sphinx}/db/fixtures/people.rb +1 -1
  31. data/features/{support → thinking_sphinx}/db/fixtures/tags.rb +1 -1
  32. data/features/{support → thinking_sphinx}/db/migrations/create_alphas.rb +1 -0
  33. data/features/{support → thinking_sphinx}/db/migrations/create_developers.rb +0 -2
  34. data/features/thinking_sphinx/db/migrations/create_genres.rb +3 -0
  35. data/features/thinking_sphinx/db/migrations/create_music.rb +6 -0
  36. data/features/thinking_sphinx/models/alpha.rb +23 -0
  37. data/features/thinking_sphinx/models/andrew.rb +17 -0
  38. data/features/{support → thinking_sphinx}/models/beta.rb +1 -1
  39. data/features/{support → thinking_sphinx}/models/developer.rb +2 -2
  40. data/features/{support → thinking_sphinx}/models/extensible_beta.rb +1 -1
  41. data/features/thinking_sphinx/models/fox.rb +5 -0
  42. data/features/thinking_sphinx/models/genre.rb +3 -0
  43. data/features/thinking_sphinx/models/medium.rb +5 -0
  44. data/features/thinking_sphinx/models/music.rb +8 -0
  45. data/features/{support → thinking_sphinx}/models/person.rb +2 -1
  46. data/features/{support → thinking_sphinx}/models/post.rb +2 -1
  47. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  48. data/lib/cucumber/thinking_sphinx/internal_world.rb +13 -11
  49. data/lib/thinking_sphinx/active_record/attribute_updates.rb +17 -15
  50. data/lib/thinking_sphinx/active_record/delta.rb +0 -26
  51. data/lib/thinking_sphinx/active_record/has_many_association.rb +34 -11
  52. data/lib/thinking_sphinx/active_record/scopes.rb +46 -3
  53. data/lib/thinking_sphinx/active_record.rb +271 -193
  54. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +45 -9
  55. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +5 -1
  56. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +9 -1
  57. data/lib/thinking_sphinx/attribute.rb +67 -23
  58. data/lib/thinking_sphinx/auto_version.rb +24 -0
  59. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  60. data/lib/thinking_sphinx/class_facet.rb +3 -2
  61. data/lib/thinking_sphinx/configuration.rb +78 -64
  62. data/lib/thinking_sphinx/context.rb +76 -0
  63. data/lib/thinking_sphinx/deltas/default_delta.rb +14 -20
  64. data/lib/thinking_sphinx/deltas.rb +0 -2
  65. data/lib/thinking_sphinx/deploy/capistrano.rb +1 -1
  66. data/lib/thinking_sphinx/excerpter.rb +1 -1
  67. data/lib/thinking_sphinx/facet.rb +6 -5
  68. data/lib/thinking_sphinx/facet_search.rb +54 -24
  69. data/lib/thinking_sphinx/field.rb +2 -4
  70. data/lib/thinking_sphinx/index/builder.rb +36 -20
  71. data/lib/thinking_sphinx/index/faux_column.rb +8 -0
  72. data/lib/thinking_sphinx/index.rb +77 -19
  73. data/lib/thinking_sphinx/join.rb +37 -0
  74. data/lib/thinking_sphinx/property.rb +9 -2
  75. data/lib/thinking_sphinx/rails_additions.rb +4 -4
  76. data/lib/thinking_sphinx/search.rb +212 -66
  77. data/lib/thinking_sphinx/search_methods.rb +22 -4
  78. data/lib/thinking_sphinx/source/internal_properties.rb +2 -2
  79. data/lib/thinking_sphinx/source/sql.rb +5 -3
  80. data/lib/thinking_sphinx/source.rb +21 -12
  81. data/lib/thinking_sphinx/tasks.rb +26 -58
  82. data/lib/thinking_sphinx/test.rb +55 -0
  83. data/lib/thinking_sphinx.rb +70 -38
  84. data/rails/init.rb +4 -2
  85. data/spec/{lib/thinking_sphinx → thinking_sphinx}/active_record/delta_spec.rb +6 -8
  86. data/spec/{lib/thinking_sphinx → thinking_sphinx}/active_record/has_many_association_spec.rb +26 -3
  87. data/spec/thinking_sphinx/active_record/scopes_spec.rb +176 -0
  88. data/spec/thinking_sphinx/active_record_spec.rb +618 -0
  89. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +134 -0
  90. data/spec/{lib/thinking_sphinx → thinking_sphinx}/association_spec.rb +1 -1
  91. data/spec/{lib/thinking_sphinx → thinking_sphinx}/attribute_spec.rb +87 -46
  92. data/spec/thinking_sphinx/auto_version_spec.rb +47 -0
  93. data/spec/{lib/thinking_sphinx → thinking_sphinx}/configuration_spec.rb +73 -63
  94. data/spec/thinking_sphinx/context_spec.rb +127 -0
  95. data/spec/{lib/thinking_sphinx → thinking_sphinx}/core/array_spec.rb +1 -1
  96. data/spec/{lib/thinking_sphinx → thinking_sphinx}/core/string_spec.rb +1 -1
  97. data/spec/{lib/thinking_sphinx → thinking_sphinx}/excerpter_spec.rb +1 -9
  98. data/spec/{lib/thinking_sphinx → thinking_sphinx}/facet_search_spec.rb +76 -82
  99. data/spec/{lib/thinking_sphinx → thinking_sphinx}/facet_spec.rb +5 -5
  100. data/spec/{lib/thinking_sphinx → thinking_sphinx}/field_spec.rb +1 -42
  101. data/spec/{lib/thinking_sphinx → thinking_sphinx}/index/builder_spec.rb +71 -31
  102. data/spec/{lib/thinking_sphinx → thinking_sphinx}/index/faux_column_spec.rb +8 -2
  103. data/spec/thinking_sphinx/index_spec.rb +183 -0
  104. data/spec/{lib/thinking_sphinx → thinking_sphinx}/rails_additions_spec.rb +5 -5
  105. data/spec/{lib/thinking_sphinx → thinking_sphinx}/search_methods_spec.rb +5 -1
  106. data/spec/{lib/thinking_sphinx → thinking_sphinx}/search_spec.rb +183 -31
  107. data/spec/{lib/thinking_sphinx → thinking_sphinx}/source_spec.rb +18 -2
  108. data/spec/thinking_sphinx/test_spec.rb +20 -0
  109. data/spec/thinking_sphinx_spec.rb +204 -0
  110. data/tasks/distribution.rb +7 -26
  111. data/tasks/testing.rb +32 -20
  112. metadata +488 -147
  113. data/VERSION.yml +0 -5
  114. data/features/datetime_deltas.feature +0 -66
  115. data/features/delayed_delta_indexing.feature +0 -37
  116. data/features/step_definitions/datetime_delta_steps.rb +0 -15
  117. data/features/step_definitions/delayed_delta_indexing_steps.rb +0 -7
  118. data/features/support/database.yml +0 -5
  119. data/features/support/db/active_record.rb +0 -40
  120. data/features/support/db/database.yml +0 -5
  121. data/features/support/db/fixtures/delayed_betas.rb +0 -10
  122. data/features/support/db/fixtures/thetas.rb +0 -10
  123. data/features/support/db/migrations/create_delayed_betas.rb +0 -17
  124. data/features/support/db/migrations/create_thetas.rb +0 -5
  125. data/features/support/db/mysql.rb +0 -3
  126. data/features/support/db/postgresql.rb +0 -3
  127. data/features/support/models/alpha.rb +0 -10
  128. data/features/support/models/delayed_beta.rb +0 -7
  129. data/features/support/models/theta.rb +0 -7
  130. data/features/support/post_database.rb +0 -43
  131. data/lib/thinking_sphinx/deltas/datetime_delta.rb +0 -50
  132. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +0 -24
  133. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +0 -27
  134. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +0 -26
  135. data/lib/thinking_sphinx/deltas/delayed_delta.rb +0 -30
  136. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +0 -96
  137. data/spec/lib/thinking_sphinx/active_record_spec.rb +0 -353
  138. data/spec/lib/thinking_sphinx/deltas/job_spec.rb +0 -32
  139. data/spec/lib/thinking_sphinx/index_spec.rb +0 -45
  140. data/spec/lib/thinking_sphinx_spec.rb +0 -162
  141. data/vendor/after_commit/LICENSE +0 -20
  142. data/vendor/after_commit/README +0 -16
  143. data/vendor/after_commit/Rakefile +0 -22
  144. data/vendor/after_commit/init.rb +0 -8
  145. data/vendor/after_commit/lib/after_commit/active_record.rb +0 -114
  146. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +0 -103
  147. data/vendor/after_commit/lib/after_commit.rb +0 -45
  148. data/vendor/after_commit/test/after_commit_test.rb +0 -53
  149. data/vendor/delayed_job/lib/delayed/job.rb +0 -251
  150. data/vendor/delayed_job/lib/delayed/message_sending.rb +0 -7
  151. data/vendor/delayed_job/lib/delayed/performable_method.rb +0 -55
  152. data/vendor/delayed_job/lib/delayed/worker.rb +0 -54
  153. data/vendor/riddle/lib/riddle/client/filter.rb +0 -53
  154. data/vendor/riddle/lib/riddle/client/message.rb +0 -66
  155. data/vendor/riddle/lib/riddle/client/response.rb +0 -84
  156. data/vendor/riddle/lib/riddle/client.rb +0 -635
  157. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +0 -48
  158. data/vendor/riddle/lib/riddle/configuration/index.rb +0 -142
  159. data/vendor/riddle/lib/riddle/configuration/indexer.rb +0 -19
  160. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +0 -17
  161. data/vendor/riddle/lib/riddle/configuration/searchd.rb +0 -25
  162. data/vendor/riddle/lib/riddle/configuration/section.rb +0 -43
  163. data/vendor/riddle/lib/riddle/configuration/source.rb +0 -23
  164. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +0 -34
  165. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +0 -28
  166. data/vendor/riddle/lib/riddle/configuration.rb +0 -33
  167. data/vendor/riddle/lib/riddle/controller.rb +0 -53
  168. data/vendor/riddle/lib/riddle.rb +0 -30
  169. data/features/{support → thinking_sphinx}/database.example.yml +0 -0
  170. data/features/{support → thinking_sphinx}/db/fixtures/alphas.rb +0 -0
  171. data/features/{support → thinking_sphinx}/db/fixtures/authors.rb +0 -0
  172. data/features/{support → thinking_sphinx}/db/fixtures/boxes.rb +0 -0
  173. data/features/{support → thinking_sphinx}/db/fixtures/categories.rb +0 -0
  174. data/features/{support → thinking_sphinx}/db/fixtures/cats.rb +0 -0
  175. data/features/{support → thinking_sphinx}/db/fixtures/dogs.rb +0 -0
  176. data/features/{support → thinking_sphinx}/db/fixtures/extensible_betas.rb +0 -0
  177. data/features/{support → thinking_sphinx}/db/fixtures/gammas.rb +0 -0
  178. data/features/{support → thinking_sphinx}/db/fixtures/posts.rb +0 -0
  179. data/features/{support → thinking_sphinx}/db/fixtures/robots.rb +0 -0
  180. data/features/{support → thinking_sphinx}/db/migrations/create_animals.rb +0 -0
  181. data/features/{support → thinking_sphinx}/db/migrations/create_authors.rb +0 -0
  182. data/features/{support → thinking_sphinx}/db/migrations/create_authors_posts.rb +0 -0
  183. data/features/{support → thinking_sphinx}/db/migrations/create_betas.rb +0 -0
  184. data/features/{support → thinking_sphinx}/db/migrations/create_boxes.rb +0 -0
  185. data/features/{support → thinking_sphinx}/db/migrations/create_categories.rb +0 -0
  186. data/features/{support → thinking_sphinx}/db/migrations/create_comments.rb +0 -0
  187. data/features/{support → thinking_sphinx}/db/migrations/create_extensible_betas.rb +0 -0
  188. data/features/{support → thinking_sphinx}/db/migrations/create_gammas.rb +0 -0
  189. data/features/{support → thinking_sphinx}/db/migrations/create_people.rb +0 -0
  190. data/features/{support → thinking_sphinx}/db/migrations/create_posts.rb +0 -0
  191. data/features/{support → thinking_sphinx}/db/migrations/create_robots.rb +0 -0
  192. data/features/{support → thinking_sphinx}/db/migrations/create_taggings.rb +0 -0
  193. data/features/{support → thinking_sphinx}/db/migrations/create_tags.rb +0 -0
  194. data/features/{support → thinking_sphinx}/models/animal.rb +0 -0
  195. data/features/{support → thinking_sphinx}/models/author.rb +0 -0
  196. data/features/{support → thinking_sphinx}/models/box.rb +0 -0
  197. data/features/{support → thinking_sphinx}/models/cat.rb +0 -0
  198. data/features/{support → thinking_sphinx}/models/category.rb +0 -0
  199. data/features/{support → thinking_sphinx}/models/comment.rb +3 -3
  200. /data/features/{support → thinking_sphinx}/models/dog.rb +0 -0
  201. /data/features/{support → thinking_sphinx}/models/gamma.rb +0 -0
  202. /data/features/{support → thinking_sphinx}/models/robot.rb +0 -0
  203. /data/features/{support → thinking_sphinx}/models/tag.rb +0 -0
  204. /data/features/{support → thinking_sphinx}/models/tagging.rb +0 -0
@@ -14,7 +14,7 @@ module ThinkingSphinx
14
14
  kind_of? member? method methods nil? object_id respond_to? send should
15
15
  type )
16
16
  SafeMethods = %w( partition private_methods protected_methods
17
- public_methods send )
17
+ public_methods send class )
18
18
 
19
19
  instance_methods.select { |method|
20
20
  method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
@@ -57,10 +57,38 @@ module ThinkingSphinx
57
57
  ThinkingSphinx.facets *args
58
58
  end
59
59
 
60
+ def self.bundle_searches(enum = nil)
61
+ bundle = ThinkingSphinx::BundledSearch.new
62
+
63
+ if enum.nil?
64
+ yield bundle
65
+ else
66
+ enum.each { |item| yield bundle, item }
67
+ end
68
+
69
+ bundle.searches
70
+ end
71
+
72
+ def self.matching_fields(fields, bitmask)
73
+ matches = []
74
+ bitstring = bitmask.to_s(2).rjust(32, '0').reverse
75
+
76
+ fields.each_with_index do |field, index|
77
+ matches << field if bitstring[index, 1] == '1'
78
+ end
79
+ matches
80
+ end
81
+
60
82
  def initialize(*args)
83
+ ThinkingSphinx.context.define_indexes
84
+
61
85
  @array = []
62
86
  @options = args.extract_options!
63
87
  @args = args
88
+
89
+ add_default_scope unless options[:ignore_default]
90
+
91
+ populate if @options[:populate]
64
92
  end
65
93
 
66
94
  def to_a
@@ -68,6 +96,12 @@ module ThinkingSphinx
68
96
  @array
69
97
  end
70
98
 
99
+ def freeze
100
+ populate
101
+ @array.freeze
102
+ self
103
+ end
104
+
71
105
  # Indication of whether the request has been made to Sphinx for the search
72
106
  # query.
73
107
  #
@@ -90,6 +124,9 @@ module ThinkingSphinx
90
124
  if is_scope?(method)
91
125
  add_scope(method, *args, &block)
92
126
  return self
127
+ elsif method == :search_count
128
+ merge_search one_class.search(*args), self.args, options
129
+ return scoped_count
93
130
  elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
94
131
  super
95
132
  elsif !SafeMethods.include?(method.to_s)
@@ -109,8 +146,8 @@ module ThinkingSphinx
109
146
  # @param [Symbol] method The method name
110
147
  # @return [Boolean] true if either Search or Array responds to the method.
111
148
  #
112
- def respond_to?(method)
113
- super || @array.respond_to?(method)
149
+ def respond_to?(method, include_private = false)
150
+ super || @array.respond_to?(method, include_private)
114
151
  end
115
152
 
116
153
  # The current page number of the result set. Defaults to 1 if no page was
@@ -146,7 +183,9 @@ module ThinkingSphinx
146
183
  # @return [Integer]
147
184
  #
148
185
  def per_page
149
- @options[:limit] || @options[:per_page] || 20
186
+ @options[:limit] ||= @options[:per_page]
187
+ @options[:limit] ||= 20
188
+ @options[:limit].to_i
150
189
  end
151
190
 
152
191
  # The total number of pages available if the results are paginated.
@@ -155,33 +194,51 @@ module ThinkingSphinx
155
194
  #
156
195
  def total_pages
157
196
  populate
197
+ return 0 if @results[:total].nil?
198
+
158
199
  @total_pages ||= (@results[:total] / per_page.to_f).ceil
159
200
  end
160
201
  # Compatibility with older versions of will_paginate
161
202
  alias_method :page_count, :total_pages
162
203
 
204
+ # Query time taken
205
+ #
206
+ # @return [Integer]
207
+ #
208
+ def query_time
209
+ populate
210
+ return 0 if @results[:time].nil?
211
+
212
+ @query_time ||= @results[:time]
213
+ end
214
+
163
215
  # The total number of search results available.
164
216
  #
165
217
  # @return [Integer]
166
218
  #
167
219
  def total_entries
168
220
  populate
221
+ return 0 if @results[:total_found].nil?
222
+
169
223
  @total_entries ||= @results[:total_found]
170
224
  end
171
225
 
172
226
  # The current page's offset, based on the number of records per page.
227
+ # Or explicit :offset if given.
173
228
  #
174
229
  # @return [Integer]
175
230
  #
176
231
  def offset
177
- (current_page - 1) * per_page
232
+ @options[:offset] || ((current_page - 1) * per_page)
178
233
  end
179
234
 
180
235
  def indexes
181
236
  return options[:index] if options[:index]
182
237
  return '*' if classes.empty?
183
238
 
184
- classes.collect { |klass| klass.sphinx_index_names }.flatten.join(',')
239
+ classes.collect { |klass|
240
+ klass.sphinx_index_names
241
+ }.flatten.uniq.join(',')
185
242
  end
186
243
 
187
244
  def each_with_groupby_and_count(&block)
@@ -201,6 +258,13 @@ module ThinkingSphinx
201
258
  end
202
259
  end
203
260
 
261
+ def each_with_match(&block)
262
+ populate
263
+ results[:matches].each_with_index do |match, index|
264
+ yield self[index], match
265
+ end
266
+ end
267
+
204
268
  def excerpt_for(string, model = nil)
205
269
  if model.nil? && one_class
206
270
  model ||= one_class
@@ -208,17 +272,66 @@ module ThinkingSphinx
208
272
 
209
273
  populate
210
274
  client.excerpts(
211
- :docs => [string],
212
- :words => results[:words].keys.join(' '),
213
- :index => "#{model.source_of_sphinx_index.sphinx_name}_core"
275
+ {
276
+ :docs => [string.to_s],
277
+ :words => results[:words].keys.join(' '),
278
+ :index => options[:index] || "#{model.source_of_sphinx_index.sphinx_name}_core"
279
+ }.merge(options[:excerpt_options] || {})
214
280
  ).first
215
281
  end
216
282
 
217
283
  def search(*args)
218
- merge_search ThinkingSphinx::Search.new(*args)
284
+ args << args.extract_options!.merge(:ignore_default => true)
285
+ merge_search ThinkingSphinx::Search.new(*args), self.args, options
219
286
  self
220
287
  end
221
288
 
289
+ def search_for_ids(*args)
290
+ args << args.extract_options!.merge(
291
+ :ignore_default => true,
292
+ :ids_only => true
293
+ )
294
+ merge_search ThinkingSphinx::Search.new(*args), self.args, options
295
+ self
296
+ end
297
+
298
+ def facets(*args)
299
+ options = args.extract_options!
300
+ merge_search self, args, options
301
+ args << options
302
+
303
+ ThinkingSphinx::FacetSearch.new *args
304
+ end
305
+
306
+ def client
307
+ client = options[:client] || config.client
308
+
309
+ prepare client
310
+ end
311
+
312
+ def append_to(client)
313
+ prepare client
314
+ client.append_query query, indexes, comment
315
+ client.reset
316
+ end
317
+
318
+ def populate_from_queue(results)
319
+ return if @populated
320
+ @populated = true
321
+ @results = results
322
+
323
+ if options[:ids_only]
324
+ replace @results[:matches].collect { |match|
325
+ match[:attributes]["sphinx_internal_id"]
326
+ }
327
+ else
328
+ replace instances_from_matches
329
+ add_excerpter
330
+ add_sphinx_attributes
331
+ add_matching_fields if client.rank_mode == :fieldmask
332
+ end
333
+ end
334
+
222
335
  private
223
336
 
224
337
  def config
@@ -231,8 +344,12 @@ module ThinkingSphinx
231
344
 
232
345
  retry_on_stale_index do
233
346
  begin
234
- log "Querying Sphinx: #{query}"
235
- @results = client.query query, indexes, comment
347
+ log "Querying: '#{query}'"
348
+ runtime = Benchmark.realtime {
349
+ @results = client.query query, indexes, comment
350
+ }
351
+ log "Found #{@results[:total_found]} results", :debug,
352
+ "Sphinx (#{sprintf("%f", runtime)}s)"
236
353
  rescue Errno::ECONNREFUSED => err
237
354
  raise ThinkingSphinx::ConnectionError,
238
355
  'Connection to Sphinx Daemon (searchd) failed.'
@@ -246,52 +363,70 @@ module ThinkingSphinx
246
363
  replace instances_from_matches
247
364
  add_excerpter
248
365
  add_sphinx_attributes
366
+ add_matching_fields if client.rank_mode == :fieldmask
249
367
  end
250
368
  end
251
369
  end
252
370
 
253
371
  def add_excerpter
254
372
  each do |object|
255
- next if object.respond_to?(:excerpts)
256
-
257
- excerpter = ThinkingSphinx::Excerpter.new self, object
258
- block = lambda { excerpter }
373
+ next if object.nil?
259
374
 
260
- object.metaclass.instance_eval do
261
- define_method(:excerpts, &block)
262
- end
375
+ object.excerpts = ThinkingSphinx::Excerpter.new self, object
263
376
  end
264
377
  end
265
378
 
266
379
  def add_sphinx_attributes
267
380
  each do |object|
268
- next if object.nil? || object.respond_to?(:sphinx_attributes)
381
+ next if object.nil?
269
382
 
270
- match = @results[:matches].detect { |match|
271
- match[:attributes]['sphinx_internal_id'] == object.
272
- primary_key_for_sphinx &&
273
- match[:attributes]['class_crc'] == object.class.to_crc32
274
- }
383
+ match = match_hash object
275
384
  next if match.nil?
276
385
 
277
- object.metaclass.instance_eval do
278
- define_method(:sphinx_attributes) { match[:attributes] }
279
- end
386
+ object.sphinx_attributes = match[:attributes]
280
387
  end
281
388
  end
282
389
 
283
- def self.log(message, method = :debug)
284
- return if ::ActiveRecord::Base.logger.nil?
285
- ::ActiveRecord::Base.logger.send method, message
390
+ def add_matching_fields
391
+ each do |object|
392
+ next if object.nil?
393
+
394
+ match = match_hash object
395
+ next if match.nil?
396
+ object.matching_fields = ThinkingSphinx::Search.matching_fields(
397
+ @results[:fields], match[:weight]
398
+ )
399
+ end
286
400
  end
287
401
 
288
- def log(message, method = :debug)
289
- self.class.log(message, method)
402
+ def match_hash(object)
403
+ @results[:matches].detect { |match|
404
+ match[:attributes]['sphinx_internal_id'] == object.
405
+ primary_key_for_sphinx &&
406
+ match[:attributes]['class_crc'] == object.class.to_crc32
407
+ }
290
408
  end
291
409
 
292
- def client
293
- client = config.client
410
+ def self.log(message, method = :debug, identifier = 'Sphinx')
411
+ return if ::ActiveRecord::Base.logger.nil?
412
+
413
+ info = ''
414
+ if ::ActiveRecord::Base.colorize_logging
415
+ identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
416
+ info << " \e[#{identifier_color}m#{identifier}\e[0m "
417
+ info << "\e[#{message_color}m#{message}\e[0m"
418
+ else
419
+ info = "#{identifier} #{message}"
420
+ end
294
421
 
422
+ ::ActiveRecord::Base.logger.send method, info
423
+ end
424
+
425
+ def log(*args)
426
+ self.class.log(*args)
427
+ end
428
+
429
+ def prepare(client)
295
430
  index_options = one_class ?
296
431
  one_class.sphinx_indexes.first.local_options : {}
297
432
 
@@ -300,11 +435,12 @@ module ThinkingSphinx
300
435
  :group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
301
436
  :rank_mode, :max_query_time, :field_weights
302
437
  ].each do |key|
303
- # puts "key: #{key}"
304
438
  value = options[key] || index_options[key]
305
- # puts "value: #{value.inspect}"
306
439
  client.send("#{key}=", value) if value
307
440
  end
441
+
442
+ # treated non-standard as :select is already used for AR queries
443
+ client.select = options[:sphinx_select] || '*'
308
444
 
309
445
  client.limit = per_page
310
446
  client.offset = offset
@@ -381,7 +517,8 @@ module ThinkingSphinx
381
517
  query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
382
518
  pre, proper, post = $`, $&, $'
383
519
  # E.g. "@foo", "/2", "~3", but not as part of a token
384
- is_operator = pre.match(%r{(\W|^)[@~/]\Z})
520
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z}) ||
521
+ pre.match(%r{(\W|^)@\([^\)]*$})
385
522
  # E.g. "foo bar", with quotes
386
523
  is_quote = proper.starts_with?('"') && proper.ends_with?('"')
387
524
  has_star = pre.ends_with?("*") || post.starts_with?("*")
@@ -495,24 +632,8 @@ module ThinkingSphinx
495
632
  filters
496
633
  end
497
634
 
498
- def condition_filters
499
- (options[:conditions] || {}).collect { |attrib, value|
500
- if attributes.include?(attrib.to_sym)
501
- puts <<-MSG
502
- Deprecation Warning: filters on attributes should be done using the :with
503
- option, not :conditions. For example:
504
- :with => {:#{attrib} => #{value.inspect}}
505
- MSG
506
- Riddle::Client::Filter.new attrib.to_s, filter_value(value)
507
- else
508
- nil
509
- end
510
- }.compact
511
- end
512
-
513
635
  def filters
514
636
  internal_filters +
515
- condition_filters +
516
637
  (options[:with] || {}).collect { |attrib, value|
517
638
  Riddle::Client::Filter.new attrib.to_s, filter_value(value)
518
639
  } +
@@ -527,14 +648,6 @@ MSG
527
648
  end
528
649
 
529
650
  # When passed a Time instance, returns the integer timestamp.
530
- #
531
- # If using Rails 2.1+, need to handle timezones to translate them back to
532
- # UTC, as that's what datetimes will be stored as by MySQL.
533
- #
534
- # in_time_zone is a method that was added for the timezone support in
535
- # Rails 2.1, which is why it's used for testing. I'm sure there's better
536
- # ways, but this does the job.
537
- #
538
651
  def filter_value(value)
539
652
  case value
540
653
  when Range
@@ -542,7 +655,7 @@ MSG
542
655
  when Array
543
656
  value.collect { |v| filter_value(v) }.flatten
544
657
  when Time
545
- value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
658
+ [value.to_i]
546
659
  when NilClass
547
660
  0
548
661
  else
@@ -608,6 +721,21 @@ MSG
608
721
  end
609
722
  end
610
723
 
724
+ def include_for_class(klass)
725
+ includes = options[:include] || klass.sphinx_index_options[:include]
726
+
727
+ case includes
728
+ when NilClass
729
+ nil
730
+ when Array
731
+ includes.select { |inc| klass.reflections[inc] }
732
+ when Symbol
733
+ klass.reflections[includes].nil? ? nil : includes
734
+ else
735
+ includes
736
+ end
737
+ end
738
+
611
739
  def instances_from_class(klass, matches)
612
740
  index_options = klass.sphinx_index_options
613
741
 
@@ -616,7 +744,7 @@ MSG
616
744
  :all,
617
745
  :joins => options[:joins],
618
746
  :conditions => {klass.primary_key_for_sphinx.to_sym => ids},
619
- :include => (options[:include] || index_options[:include]),
747
+ :include => include_for_class(klass),
620
748
  :select => (options[:select] || index_options[:select]),
621
749
  :order => (options[:sql_order] || index_options[:sql_order])
622
750
  ) : []
@@ -684,11 +812,18 @@ MSG
684
812
  one_class && one_class.sphinx_scopes.include?(method)
685
813
  end
686
814
 
815
+ # Adds the default_sphinx_scope if set.
816
+ def add_default_scope
817
+ return unless one_class && one_class.has_default_sphinx_scope?
818
+ add_scope(one_class.get_default_sphinx_scope.to_sym)
819
+ end
820
+
687
821
  def add_scope(method, *args, &block)
688
- merge_search one_class.send(method, *args, &block)
822
+ method = "#{method}_without_default".to_sym
823
+ merge_search one_class.send(method, *args, &block), self.args, options
689
824
  end
690
825
 
691
- def merge_search(search)
826
+ def merge_search(search, args, options)
692
827
  search.args.each { |arg| args << arg }
693
828
 
694
829
  search.options.keys.each do |key|
@@ -704,5 +839,16 @@ MSG
704
839
  end
705
840
  end
706
841
  end
842
+
843
+ def scoped_count
844
+ return self.total_entries if @options[:ids_only]
845
+
846
+ @options[:ids_only] = true
847
+ results_count = self.total_entries
848
+ @options[:ids_only] = false
849
+ @populated = false
850
+
851
+ results_count
852
+ end
707
853
  end
708
854
  end
@@ -155,7 +155,7 @@ module ThinkingSphinx
155
155
  # asterisks. You need to make the config/sphinx.yml changes yourself.
156
156
  #
157
157
  # By default, the tokens are assumed to match the regular expression
158
- # /\w+/u. If you've modified the charset_table, pass another regular
158
+ # /\w\+/u\+. If you've modified the charset_table, pass another regular
159
159
  # expression, e.g.
160
160
  #
161
161
  # User.search("oo@bar.c", :star => /[\w@.]+/u)
@@ -313,13 +313,31 @@ module ThinkingSphinx
313
313
  # Once you've got your results set, you can access the distances as
314
314
  # follows:
315
315
  #
316
- # @results.each_with_geodist do |result, distance|
317
- # # ...
318
- # end
316
+ # @results.each_with_geodist do |result, distance|
317
+ # # ...
318
+ # end
319
319
  #
320
320
  # The distance value is returned as a float, representing the distance in
321
321
  # metres.
322
322
  #
323
+ # == Filtering by custom attributes
324
+ #
325
+ # Do note that this applies only to sphinx 0.9.9
326
+ #
327
+ # Should you find yourself in desperate need of a filter that involves
328
+ # selecting either one of multiple conditions, one solution could be
329
+ # provided by the :sphinx_select option within the search.
330
+ # This handles which fields are selected by sphinx from its store.
331
+ #
332
+ # The default value is "*", and you can add custom fields using syntax
333
+ # similar to sql:
334
+ #
335
+ # Flower.search "foo",
336
+ # :sphinx_select => "*, petals < 1 or color = 2 as grass"
337
+ #
338
+ # This will add the 'grass' attribute, which will now be usable in your
339
+ # filters.
340
+ #
323
341
  # == Handling a Stale Index
324
342
  #
325
343
  # Especially if you don't use delta indexing, you risk having records in
@@ -2,9 +2,9 @@ module ThinkingSphinx
2
2
  class Source
3
3
  module InternalProperties
4
4
  def add_internal_attributes_and_facets
5
- add_internal_attribute :sphinx_internal_id, :integer, @model.primary_key_for_sphinx.to_sym
5
+ add_internal_attribute :sphinx_internal_id, nil,
6
+ @model.primary_key_for_sphinx.to_sym
6
7
  add_internal_attribute :class_crc, :integer, crc_column, true
7
- add_internal_attribute :subclass_crcs, :multi, subclasses_to_s
8
8
  add_internal_attribute :sphinx_deleted, :integer, "0"
9
9
 
10
10
  add_internal_facet :class_crc
@@ -13,8 +13,10 @@ module ThinkingSphinx
13
13
  # source.to_sql(:delta => true)
14
14
  #
15
15
  def to_sql(options={})
16
- sql = <<-SQL
17
- SELECT #{ sql_select_clause options[:offset] }
16
+ sql = "SELECT "
17
+ sql += "SQL_NO_CACHE " if adapter.sphinx_identifier == "mysql"
18
+ sql += <<-SQL
19
+ #{ sql_select_clause options[:offset] }
18
20
  FROM #{ @model.quoted_table_name }
19
21
  #{ all_associations.collect { |assoc| assoc.to_sql }.join(' ') }
20
22
  #{ sql_where_clause(options) }
@@ -53,7 +55,7 @@ GROUP BY #{ sql_group_clause }
53
55
  #
54
56
  def to_sql_query_info(offset)
55
57
  "SELECT * FROM #{@model.quoted_table_name} WHERE " +
56
- "#{quote_column(@model.primary_key_for_sphinx)} = (($id - #{offset}) / #{ThinkingSphinx.indexed_models.size})"
58
+ "#{quote_column(@model.primary_key_for_sphinx)} = (($id - #{offset}) / #{ThinkingSphinx.context.indexed_models.size})"
57
59
  end
58
60
 
59
61
  def sql_select_clause(offset)
@@ -6,19 +6,22 @@ module ThinkingSphinx
6
6
  include ThinkingSphinx::Source::InternalProperties
7
7
  include ThinkingSphinx::Source::SQL
8
8
 
9
- attr_accessor :model, :fields, :attributes, :conditions, :groupings,
9
+ attr_accessor :model, :fields, :attributes, :joins, :conditions, :groupings,
10
10
  :options
11
- attr_reader :base, :index
11
+ attr_reader :base, :index, :database_configuration
12
12
 
13
13
  def initialize(index, options = {})
14
14
  @index = index
15
15
  @model = index.model
16
16
  @fields = []
17
17
  @attributes = []
18
+ @joins = []
18
19
  @conditions = []
19
20
  @groupings = []
20
21
  @options = options
21
22
  @associations = {}
23
+ @database_configuration = @model.connection.
24
+ instance_variable_get(:@config).clone
22
25
 
23
26
  @base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(
24
27
  @model, [], nil
@@ -33,30 +36,31 @@ module ThinkingSphinx
33
36
  end
34
37
 
35
38
  def name
36
- @model.sphinx_name
39
+ index.name
37
40
  end
38
41
 
39
- def to_riddle_for_core(offset, index)
42
+ def to_riddle_for_core(offset, position)
40
43
  source = Riddle::Configuration::SQLSource.new(
41
- "#{name}_core_#{index}", adapter.sphinx_identifier
44
+ "#{index.core_name}_#{position}", adapter.sphinx_identifier
42
45
  )
43
46
 
44
47
  set_source_database_settings source
45
48
  set_source_attributes source, offset
46
- set_source_sql source, offset
47
49
  set_source_settings source
50
+ set_source_sql source, offset
48
51
 
49
52
  source
50
53
  end
51
54
 
52
- def to_riddle_for_delta(offset, index)
55
+ def to_riddle_for_delta(offset, position)
53
56
  source = Riddle::Configuration::SQLSource.new(
54
- "#{name}_delta_#{index}", adapter.sphinx_identifier
57
+ "#{index.delta_name}_#{position}", adapter.sphinx_identifier
55
58
  )
56
- source.parent = "#{name}_core_#{index}"
59
+ source.parent = "#{index.core_name}_#{position}"
57
60
 
58
61
  set_source_database_settings source
59
62
  set_source_attributes source, offset, true
63
+ set_source_settings source
60
64
  set_source_sql source, offset, true
61
65
 
62
66
  source
@@ -79,7 +83,7 @@ module ThinkingSphinx
79
83
  end
80
84
 
81
85
  def set_source_database_settings(source)
82
- config = @model.connection.instance_variable_get(:@config)
86
+ config = @database_configuration
83
87
 
84
88
  source.sql_host = config[:host] || "localhost"
85
89
  source.sql_user = config[:username] || config[:user] || 'root'
@@ -107,6 +111,7 @@ module ThinkingSphinx
107
111
  end
108
112
 
109
113
  source.sql_query_pre += [adapter.utf8_query_pre].compact if utf8?
114
+ source.sql_query_pre << adapter.utc_query_pre
110
115
  end
111
116
 
112
117
  def set_source_settings(source)
@@ -136,7 +141,11 @@ module ThinkingSphinx
136
141
  # attribute associations
137
142
  @attributes.collect { |attrib|
138
143
  attrib.associations.values if attrib.include_as_association?
139
- }.compact.flatten
144
+ }.compact.flatten +
145
+ # explicit joins
146
+ @joins.collect { |join|
147
+ join.associations
148
+ }.flatten
140
149
  ).uniq.collect { |assoc|
141
150
  # get ancestors as well as column-level associations
142
151
  assoc.ancestors
@@ -144,7 +153,7 @@ module ThinkingSphinx
144
153
  end
145
154
 
146
155
  def utf8?
147
- @index.options[:charset_type] == "utf-8"
156
+ @index.options[:charset_type] =~ /utf-8|zh_cn.utf-8/
148
157
  end
149
158
  end
150
159
  end