skalee-thinking-sphinx 1.3.14.1

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 (199) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +201 -0
  3. data/Rakefile +3 -0
  4. data/VERSION +1 -0
  5. data/contribute.rb +385 -0
  6. data/cucumber.yml +1 -0
  7. data/features/abstract_inheritance.feature +10 -0
  8. data/features/alternate_primary_key.feature +27 -0
  9. data/features/attribute_transformation.feature +22 -0
  10. data/features/attribute_updates.feature +51 -0
  11. data/features/deleting_instances.feature +67 -0
  12. data/features/direct_attributes.feature +11 -0
  13. data/features/excerpts.feature +13 -0
  14. data/features/extensible_delta_indexing.feature +9 -0
  15. data/features/facets.feature +82 -0
  16. data/features/facets_across_model.feature +29 -0
  17. data/features/handling_edits.feature +92 -0
  18. data/features/retry_stale_indexes.feature +24 -0
  19. data/features/searching_across_models.feature +20 -0
  20. data/features/searching_by_index.feature +40 -0
  21. data/features/searching_by_model.feature +175 -0
  22. data/features/searching_with_find_arguments.feature +56 -0
  23. data/features/sphinx_detection.feature +25 -0
  24. data/features/sphinx_scopes.feature +42 -0
  25. data/features/step_definitions/alpha_steps.rb +16 -0
  26. data/features/step_definitions/beta_steps.rb +7 -0
  27. data/features/step_definitions/common_steps.rb +188 -0
  28. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  29. data/features/step_definitions/facet_steps.rb +96 -0
  30. data/features/step_definitions/find_arguments_steps.rb +36 -0
  31. data/features/step_definitions/gamma_steps.rb +15 -0
  32. data/features/step_definitions/scope_steps.rb +15 -0
  33. data/features/step_definitions/search_steps.rb +89 -0
  34. data/features/step_definitions/sphinx_steps.rb +35 -0
  35. data/features/sti_searching.feature +19 -0
  36. data/features/support/database.example.yml +3 -0
  37. data/features/support/db/.gitignore +1 -0
  38. data/features/support/db/fixtures/alphas.rb +10 -0
  39. data/features/support/db/fixtures/authors.rb +1 -0
  40. data/features/support/db/fixtures/betas.rb +10 -0
  41. data/features/support/db/fixtures/boxes.rb +9 -0
  42. data/features/support/db/fixtures/categories.rb +1 -0
  43. data/features/support/db/fixtures/cats.rb +3 -0
  44. data/features/support/db/fixtures/comments.rb +24 -0
  45. data/features/support/db/fixtures/developers.rb +29 -0
  46. data/features/support/db/fixtures/dogs.rb +3 -0
  47. data/features/support/db/fixtures/extensible_betas.rb +10 -0
  48. data/features/support/db/fixtures/foxes.rb +3 -0
  49. data/features/support/db/fixtures/gammas.rb +10 -0
  50. data/features/support/db/fixtures/music.rb +4 -0
  51. data/features/support/db/fixtures/people.rb +1001 -0
  52. data/features/support/db/fixtures/posts.rb +6 -0
  53. data/features/support/db/fixtures/robots.rb +14 -0
  54. data/features/support/db/fixtures/tags.rb +27 -0
  55. data/features/support/db/migrations/create_alphas.rb +8 -0
  56. data/features/support/db/migrations/create_animals.rb +5 -0
  57. data/features/support/db/migrations/create_authors.rb +3 -0
  58. data/features/support/db/migrations/create_authors_posts.rb +6 -0
  59. data/features/support/db/migrations/create_betas.rb +5 -0
  60. data/features/support/db/migrations/create_boxes.rb +5 -0
  61. data/features/support/db/migrations/create_categories.rb +3 -0
  62. data/features/support/db/migrations/create_comments.rb +10 -0
  63. data/features/support/db/migrations/create_developers.rb +9 -0
  64. data/features/support/db/migrations/create_extensible_betas.rb +5 -0
  65. data/features/support/db/migrations/create_gammas.rb +3 -0
  66. data/features/support/db/migrations/create_genres.rb +3 -0
  67. data/features/support/db/migrations/create_music.rb +6 -0
  68. data/features/support/db/migrations/create_people.rb +13 -0
  69. data/features/support/db/migrations/create_posts.rb +5 -0
  70. data/features/support/db/migrations/create_robots.rb +4 -0
  71. data/features/support/db/migrations/create_taggings.rb +5 -0
  72. data/features/support/db/migrations/create_tags.rb +4 -0
  73. data/features/support/env.rb +21 -0
  74. data/features/support/lib/generic_delta_handler.rb +8 -0
  75. data/features/support/models/alpha.rb +22 -0
  76. data/features/support/models/animal.rb +5 -0
  77. data/features/support/models/author.rb +3 -0
  78. data/features/support/models/beta.rb +8 -0
  79. data/features/support/models/box.rb +8 -0
  80. data/features/support/models/cat.rb +3 -0
  81. data/features/support/models/category.rb +4 -0
  82. data/features/support/models/comment.rb +10 -0
  83. data/features/support/models/developer.rb +16 -0
  84. data/features/support/models/dog.rb +3 -0
  85. data/features/support/models/extensible_beta.rb +9 -0
  86. data/features/support/models/fox.rb +5 -0
  87. data/features/support/models/gamma.rb +5 -0
  88. data/features/support/models/genre.rb +3 -0
  89. data/features/support/models/medium.rb +5 -0
  90. data/features/support/models/music.rb +8 -0
  91. data/features/support/models/person.rb +23 -0
  92. data/features/support/models/post.rb +21 -0
  93. data/features/support/models/robot.rb +12 -0
  94. data/features/support/models/tag.rb +3 -0
  95. data/features/support/models/tagging.rb +4 -0
  96. data/ginger_scenarios.rb +28 -0
  97. data/init.rb +5 -0
  98. data/install.rb +5 -0
  99. data/lib/cucumber/thinking_sphinx/external_world.rb +8 -0
  100. data/lib/cucumber/thinking_sphinx/internal_world.rb +126 -0
  101. data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
  102. data/lib/thinking_sphinx/active_record/attribute_updates.rb +19 -0
  103. data/lib/thinking_sphinx/active_record/delta.rb +47 -0
  104. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  105. data/lib/thinking_sphinx/active_record/scopes.rb +75 -0
  106. data/lib/thinking_sphinx/active_record.rb +348 -0
  107. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  108. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  109. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +143 -0
  110. data/lib/thinking_sphinx/association.rb +164 -0
  111. data/lib/thinking_sphinx/attribute.rb +362 -0
  112. data/lib/thinking_sphinx/auto_version.rb +22 -0
  113. data/lib/thinking_sphinx/class_facet.rb +15 -0
  114. data/lib/thinking_sphinx/configuration.rb +300 -0
  115. data/lib/thinking_sphinx/context.rb +68 -0
  116. data/lib/thinking_sphinx/core/array.rb +7 -0
  117. data/lib/thinking_sphinx/core/string.rb +15 -0
  118. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  119. data/lib/thinking_sphinx/deltas.rb +28 -0
  120. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  121. data/lib/thinking_sphinx/excerpter.rb +22 -0
  122. data/lib/thinking_sphinx/facet.rb +125 -0
  123. data/lib/thinking_sphinx/facet_search.rb +136 -0
  124. data/lib/thinking_sphinx/field.rb +82 -0
  125. data/lib/thinking_sphinx/index/builder.rb +296 -0
  126. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  127. data/lib/thinking_sphinx/index.rb +157 -0
  128. data/lib/thinking_sphinx/property.rb +162 -0
  129. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  130. data/lib/thinking_sphinx/search.rb +769 -0
  131. data/lib/thinking_sphinx/search_methods.rb +439 -0
  132. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  133. data/lib/thinking_sphinx/source/sql.rb +130 -0
  134. data/lib/thinking_sphinx/source.rb +153 -0
  135. data/lib/thinking_sphinx/tasks.rb +131 -0
  136. data/lib/thinking_sphinx/test.rb +52 -0
  137. data/lib/thinking_sphinx.rb +225 -0
  138. data/rails/init.rb +16 -0
  139. data/recipes/thinking_sphinx.rb +3 -0
  140. data/spec/fixtures/data.sql +32 -0
  141. data/spec/fixtures/database.yml.default +3 -0
  142. data/spec/fixtures/models.rb +145 -0
  143. data/spec/fixtures/structure.sql +125 -0
  144. data/spec/spec_helper.rb +60 -0
  145. data/spec/sphinx_helper.rb +81 -0
  146. data/spec/thinking_sphinx/active_record/delta_spec.rb +128 -0
  147. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +55 -0
  148. data/spec/thinking_sphinx/active_record/scopes_spec.rb +177 -0
  149. data/spec/thinking_sphinx/active_record_spec.rb +622 -0
  150. data/spec/thinking_sphinx/association_spec.rb +239 -0
  151. data/spec/thinking_sphinx/attribute_spec.rb +570 -0
  152. data/spec/thinking_sphinx/auto_version_spec.rb +39 -0
  153. data/spec/thinking_sphinx/configuration_spec.rb +234 -0
  154. data/spec/thinking_sphinx/context_spec.rb +119 -0
  155. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  156. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  157. data/spec/thinking_sphinx/excerpter_spec.rb +57 -0
  158. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  159. data/spec/thinking_sphinx/facet_spec.rb +333 -0
  160. data/spec/thinking_sphinx/field_spec.rb +154 -0
  161. data/spec/thinking_sphinx/index/builder_spec.rb +479 -0
  162. data/spec/thinking_sphinx/index/faux_column_spec.rb +30 -0
  163. data/spec/thinking_sphinx/index_spec.rb +183 -0
  164. data/spec/thinking_sphinx/rails_additions_spec.rb +203 -0
  165. data/spec/thinking_sphinx/search_methods_spec.rb +152 -0
  166. data/spec/thinking_sphinx/search_spec.rb +1181 -0
  167. data/spec/thinking_sphinx/source_spec.rb +235 -0
  168. data/spec/thinking_sphinx_spec.rb +204 -0
  169. data/tasks/distribution.rb +41 -0
  170. data/tasks/rails.rake +1 -0
  171. data/tasks/testing.rb +72 -0
  172. data/vendor/after_commit/.gitignore +1 -0
  173. data/vendor/after_commit/lib/after_commit/active_record.rb +122 -0
  174. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +168 -0
  175. data/vendor/after_commit/lib/after_commit/test_bypass.rb +30 -0
  176. data/vendor/after_commit/lib/after_commit.rb +70 -0
  177. data/vendor/riddle/lib/riddle/0.9.8.rb +1 -0
  178. data/vendor/riddle/lib/riddle/0.9.9/client/filter.rb +22 -0
  179. data/vendor/riddle/lib/riddle/0.9.9/client.rb +49 -0
  180. data/vendor/riddle/lib/riddle/0.9.9/configuration/searchd.rb +28 -0
  181. data/vendor/riddle/lib/riddle/0.9.9.rb +7 -0
  182. data/vendor/riddle/lib/riddle/auto_version.rb +11 -0
  183. data/vendor/riddle/lib/riddle/client/filter.rb +62 -0
  184. data/vendor/riddle/lib/riddle/client/message.rb +70 -0
  185. data/vendor/riddle/lib/riddle/client/response.rb +94 -0
  186. data/vendor/riddle/lib/riddle/client.rb +745 -0
  187. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +49 -0
  188. data/vendor/riddle/lib/riddle/configuration/index.rb +149 -0
  189. data/vendor/riddle/lib/riddle/configuration/indexer.rb +20 -0
  190. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  191. data/vendor/riddle/lib/riddle/configuration/searchd.rb +28 -0
  192. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  193. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  194. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +53 -0
  195. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +29 -0
  196. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  197. data/vendor/riddle/lib/riddle/controller.rb +78 -0
  198. data/vendor/riddle/lib/riddle.rb +51 -0
  199. metadata +312 -0
@@ -0,0 +1,769 @@
1
+ # encoding: UTF-8
2
+ module ThinkingSphinx
3
+ # Once you've got those indexes in and built, this is the stuff that
4
+ # matters - how to search! This class provides a generic search
5
+ # interface - which you can use to search all your indexed models at once.
6
+ # Most times, you will just want a specific model's results - to search and
7
+ # search_for_ids methods will do the job in exactly the same manner when
8
+ # called from a model.
9
+ #
10
+ class Search
11
+ CoreMethods = %w( == class class_eval extend frozen? id instance_eval
12
+ instance_of? instance_values instance_variable_defined?
13
+ instance_variable_get instance_variable_set instance_variables is_a?
14
+ kind_of? member? method methods nil? object_id respond_to? send should
15
+ type )
16
+ SafeMethods = %w( partition private_methods protected_methods
17
+ public_methods send class )
18
+
19
+ instance_methods.select { |method|
20
+ method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
21
+ }.each { |method|
22
+ undef_method method
23
+ }
24
+
25
+ HashOptions = [:conditions, :with, :without, :with_all]
26
+ ArrayOptions = [:classes, :without_ids]
27
+
28
+ attr_reader :args, :options
29
+
30
+ # Deprecated. Use ThinkingSphinx.search
31
+ def self.search(*args)
32
+ log 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
33
+ ThinkingSphinx.search *args
34
+ end
35
+
36
+ # Deprecated. Use ThinkingSphinx.search_for_ids
37
+ def self.search_for_ids(*args)
38
+ log 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
39
+ ThinkingSphinx.search_for_ids *args
40
+ end
41
+
42
+ # Deprecated. Use ThinkingSphinx.search_for_ids
43
+ def self.search_for_id(*args)
44
+ log 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
45
+ ThinkingSphinx.search_for_id *args
46
+ end
47
+
48
+ # Deprecated. Use ThinkingSphinx.count
49
+ def self.count(*args)
50
+ log 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
51
+ ThinkingSphinx.count *args
52
+ end
53
+
54
+ # Deprecated. Use ThinkingSphinx.facets
55
+ def self.facets(*args)
56
+ log 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
57
+ ThinkingSphinx.facets *args
58
+ end
59
+
60
+ def self.matching_fields(fields, bitmask)
61
+ matches = []
62
+ bitstring = bitmask.to_s(2).rjust(32, '0').reverse
63
+
64
+ fields.each_with_index do |field, index|
65
+ matches << field if bitstring[index, 1] == '1'
66
+ end
67
+ matches
68
+ end
69
+
70
+ def initialize(*args)
71
+ ThinkingSphinx.context.define_indexes
72
+
73
+ @array = []
74
+ @options = args.extract_options!
75
+ @args = args
76
+ end
77
+
78
+ def to_a
79
+ populate
80
+ @array
81
+ end
82
+
83
+ # Indication of whether the request has been made to Sphinx for the search
84
+ # query.
85
+ #
86
+ # @return [Boolean] true if the results have been requested.
87
+ #
88
+ def populated?
89
+ !!@populated
90
+ end
91
+
92
+ # The query result hash from Riddle.
93
+ #
94
+ # @return [Hash] Raw Sphinx results
95
+ #
96
+ def results
97
+ populate
98
+ @results
99
+ end
100
+
101
+ def method_missing(method, *args, &block)
102
+ if is_scope?(method)
103
+ add_scope(method, *args, &block)
104
+ return self
105
+ elsif method == :search_count
106
+ return scoped_count
107
+ elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
108
+ super
109
+ elsif !SafeMethods.include?(method.to_s)
110
+ populate
111
+ end
112
+
113
+ if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
114
+ each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
115
+ else
116
+ @array.send(method, *args, &block)
117
+ end
118
+ end
119
+
120
+ # Returns true if the Search object or the underlying Array object respond
121
+ # to the requested method.
122
+ #
123
+ # @param [Symbol] method The method name
124
+ # @return [Boolean] true if either Search or Array responds to the method.
125
+ #
126
+ def respond_to?(method)
127
+ super || @array.respond_to?(method)
128
+ end
129
+
130
+ # The current page number of the result set. Defaults to 1 if no page was
131
+ # explicitly requested.
132
+ #
133
+ # @return [Integer]
134
+ #
135
+ def current_page
136
+ @options[:page].blank? ? 1 : @options[:page].to_i
137
+ end
138
+
139
+ # The next page number of the result set. If there are no more pages
140
+ # available, nil is returned.
141
+ #
142
+ # @return [Integer, nil]
143
+ #
144
+ def next_page
145
+ current_page >= total_pages ? nil : current_page + 1
146
+ end
147
+
148
+ # The previous page number of the result set. If this is the first page,
149
+ # then nil is returned.
150
+ #
151
+ # @return [Integer, nil]
152
+ #
153
+ def previous_page
154
+ current_page == 1 ? nil : current_page - 1
155
+ end
156
+
157
+ # The amount of records per set of paged results. Defaults to 20 unless a
158
+ # specific page size is requested.
159
+ #
160
+ # @return [Integer]
161
+ #
162
+ def per_page
163
+ @options[:limit] ||= @options[:per_page]
164
+ @options[:limit] ||= 20
165
+ @options[:limit].to_i
166
+ end
167
+
168
+ # The total number of pages available if the results are paginated.
169
+ #
170
+ # @return [Integer]
171
+ #
172
+ def total_pages
173
+ populate
174
+ return 0 if @results[:total].nil?
175
+
176
+ @total_pages ||= (@results[:total] / per_page.to_f).ceil
177
+ end
178
+ # Compatibility with older versions of will_paginate
179
+ alias_method :page_count, :total_pages
180
+
181
+ # The total number of search results available.
182
+ #
183
+ # @return [Integer]
184
+ #
185
+ def total_entries
186
+ populate
187
+ return 0 if @results[:total_found].nil?
188
+
189
+ @total_entries ||= @results[:total_found]
190
+ end
191
+
192
+ # The current page's offset, based on the number of records per page.
193
+ # Or explicit :offset if given.
194
+ #
195
+ # @return [Integer]
196
+ #
197
+ def offset
198
+ @options[:offset] || ((current_page - 1) * per_page)
199
+ end
200
+
201
+ def indexes
202
+ return options[:index] if options[:index]
203
+ return '*' if classes.empty?
204
+
205
+ classes.collect { |klass| klass.sphinx_index_names }.flatten.join(',')
206
+ end
207
+
208
+ def each_with_groupby_and_count(&block)
209
+ populate
210
+ results[:matches].each_with_index do |match, index|
211
+ yield self[index],
212
+ match[:attributes]["@groupby"],
213
+ match[:attributes]["@count"]
214
+ end
215
+ end
216
+ alias_method :each_with_group_and_count, :each_with_groupby_and_count
217
+
218
+ def each_with_weighting(&block)
219
+ populate
220
+ results[:matches].each_with_index do |match, index|
221
+ yield self[index], match[:weight]
222
+ end
223
+ end
224
+
225
+ def excerpt_for(string, model = nil)
226
+ if model.nil? && one_class
227
+ model ||= one_class
228
+ end
229
+
230
+ populate
231
+ client.excerpts(
232
+ :docs => [string],
233
+ :words => results[:words].keys.join(' '),
234
+ :index => "#{model.source_of_sphinx_index.sphinx_name}_core"
235
+ ).first
236
+ end
237
+
238
+ def search(*args)
239
+ add_default_scope
240
+ merge_search ThinkingSphinx::Search.new(*args)
241
+ self
242
+ end
243
+
244
+ private
245
+
246
+ def config
247
+ ThinkingSphinx::Configuration.instance
248
+ end
249
+
250
+ def populate
251
+ return if @populated
252
+ @populated = true
253
+
254
+ retry_on_stale_index do
255
+ begin
256
+ log "Querying: '#{query}'"
257
+ runtime = Benchmark.realtime {
258
+ @results = client.query query, indexes, comment
259
+ }
260
+ log "Found #{@results[:total_found]} results", :debug,
261
+ "Sphinx (#{sprintf("%f", runtime)}s)"
262
+ rescue Errno::ECONNREFUSED => err
263
+ raise ThinkingSphinx::ConnectionError,
264
+ 'Connection to Sphinx Daemon (searchd) failed.'
265
+ end
266
+
267
+ if options[:ids_only]
268
+ replace @results[:matches].collect { |match|
269
+ match[:attributes]["sphinx_internal_id"]
270
+ }
271
+ else
272
+ replace instances_from_matches
273
+ add_excerpter
274
+ add_sphinx_attributes
275
+ add_matching_fields if client.rank_mode == :fieldmask
276
+ end
277
+ end
278
+ end
279
+
280
+ def add_excerpter
281
+ each do |object|
282
+ next if object.respond_to?(:excerpts)
283
+
284
+ excerpter = ThinkingSphinx::Excerpter.new self, object
285
+ block = lambda { excerpter }
286
+
287
+ object.singleton_class.instance_eval do
288
+ define_method(:excerpts, &block)
289
+ end
290
+ end
291
+ end
292
+
293
+ def add_sphinx_attributes
294
+ each do |object|
295
+ next if object.nil? || object.respond_to?(:sphinx_attributes)
296
+
297
+ match = match_hash object
298
+ next if match.nil?
299
+
300
+ object.singleton_class.instance_eval do
301
+ define_method(:sphinx_attributes) { match[:attributes] }
302
+ end
303
+ end
304
+ end
305
+
306
+ def add_matching_fields
307
+ each do |object|
308
+ next if object.nil? || object.respond_to?(:matching_fields)
309
+
310
+ match = match_hash object
311
+ next if match.nil?
312
+ fields = ThinkingSphinx::Search.matching_fields(
313
+ @results[:fields], match[:weight]
314
+ )
315
+
316
+ object.singleton_class.instance_eval do
317
+ define_method(:matching_fields) { fields }
318
+ end
319
+ end
320
+ end
321
+
322
+ def match_hash(object)
323
+ @results[:matches].detect { |match|
324
+ match[:attributes]['sphinx_internal_id'] == object.
325
+ primary_key_for_sphinx &&
326
+ match[:attributes]['class_crc'] == object.class.to_crc32
327
+ }
328
+ end
329
+
330
+ def self.log(message, method = :debug, identifier = 'Sphinx')
331
+ return if ::ActiveRecord::Base.logger.nil?
332
+ identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
333
+ info = " \e[#{identifier_color}m#{identifier}\e[0m "
334
+ info << "\e[#{message_color}m#{message}\e[0m"
335
+ ::ActiveRecord::Base.logger.send method, info
336
+ end
337
+
338
+ def log(*args)
339
+ self.class.log(*args)
340
+ end
341
+
342
+ def client
343
+ client = config.client
344
+
345
+ index_options = one_class ?
346
+ one_class.sphinx_indexes.first.local_options : {}
347
+
348
+ [
349
+ :max_matches, :group_by, :group_function, :group_clause,
350
+ :group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
351
+ :rank_mode, :max_query_time, :field_weights
352
+ ].each do |key|
353
+ value = options[key] || index_options[key]
354
+ client.send("#{key}=", value) if value
355
+ end
356
+
357
+ # treated non-standard as :select is already used for AR queries
358
+ client.select = options[:sphinx_select] || '*'
359
+
360
+ client.limit = per_page
361
+ client.offset = offset
362
+ client.match_mode = match_mode
363
+ client.filters = filters
364
+ client.sort_mode = sort_mode
365
+ client.sort_by = sort_by
366
+ client.group_by = group_by if group_by
367
+ client.group_function = group_function if group_function
368
+ client.index_weights = index_weights
369
+ client.anchor = anchor
370
+
371
+ client
372
+ end
373
+
374
+ def retry_on_stale_index(&block)
375
+ stale_ids = []
376
+ retries = stale_retries
377
+
378
+ begin
379
+ options[:raise_on_stale] = retries > 0
380
+ block.call
381
+
382
+ # If ThinkingSphinx::Search#instances_from_matches found records in
383
+ # Sphinx but not in the DB and the :raise_on_stale option is set, this
384
+ # exception is raised. We retry a limited number of times, excluding the
385
+ # stale ids from the search.
386
+ rescue StaleIdsException => err
387
+ retries -= 1
388
+
389
+ # For logging
390
+ stale_ids |= err.ids
391
+ # ID exclusion
392
+ options[:without_ids] = Array(options[:without_ids]) | err.ids
393
+
394
+ log 'Sphinx Stale Ids (%s %s left): %s' % [
395
+ retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
396
+ ]
397
+ retry
398
+ end
399
+ end
400
+
401
+ def classes
402
+ @classes ||= options[:classes] || []
403
+ end
404
+
405
+ def one_class
406
+ @one_class ||= classes.length != 1 ? nil : classes.first
407
+ end
408
+
409
+ def query
410
+ @query ||= begin
411
+ q = @args.join(' ') << conditions_as_query
412
+ (options[:star] ? star_query(q) : q).strip
413
+ end
414
+ end
415
+
416
+ def conditions_as_query
417
+ return '' if @options[:conditions].blank?
418
+
419
+ # Soon to be deprecated.
420
+ keys = @options[:conditions].keys.reject { |key|
421
+ attributes.include?(key.to_sym)
422
+ }
423
+
424
+ ' ' + keys.collect { |key|
425
+ "@#{key} #{options[:conditions][key]}"
426
+ }.join(' ')
427
+ end
428
+
429
+ def star_query(query)
430
+ token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
431
+
432
+ query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
433
+ pre, proper, post = $`, $&, $'
434
+ # E.g. "@foo", "/2", "~3", but not as part of a token
435
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z})
436
+ # E.g. "foo bar", with quotes
437
+ is_quote = proper.starts_with?('"') && proper.ends_with?('"')
438
+ has_star = pre.ends_with?("*") || post.starts_with?("*")
439
+ if is_operator || is_quote || has_star
440
+ proper
441
+ else
442
+ "*#{proper}*"
443
+ end
444
+ end
445
+ end
446
+
447
+ def comment
448
+ options[:comment] || ''
449
+ end
450
+
451
+ def match_mode
452
+ options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
453
+ end
454
+
455
+ def sort_mode
456
+ @sort_mode ||= case options[:sort_mode]
457
+ when :asc
458
+ :attr_asc
459
+ when :desc
460
+ :attr_desc
461
+ when nil
462
+ case options[:order]
463
+ when String
464
+ :extended
465
+ when Symbol
466
+ :attr_asc
467
+ else
468
+ :relevance
469
+ end
470
+ else
471
+ options[:sort_mode]
472
+ end
473
+ end
474
+
475
+ def sort_by
476
+ case @sort_by = (options[:sort_by] || options[:order])
477
+ when String
478
+ sorted_fields_to_attributes(@sort_by)
479
+ when Symbol
480
+ field_names.include?(@sort_by) ?
481
+ @sort_by.to_s.concat('_sort') : @sort_by.to_s
482
+ else
483
+ ''
484
+ end
485
+ end
486
+
487
+ def field_names
488
+ return [] unless one_class
489
+
490
+ one_class.sphinx_indexes.collect { |index|
491
+ index.fields.collect { |field| field.unique_name }
492
+ }.flatten
493
+ end
494
+
495
+ def sorted_fields_to_attributes(order_string)
496
+ field_names.each { |field|
497
+ order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
498
+ match.gsub field.to_s, field.to_s.concat("_sort")
499
+ }
500
+ }
501
+
502
+ order_string
503
+ end
504
+
505
+ # Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
506
+ # { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
507
+ #
508
+ def index_weights
509
+ weights = options[:index_weights] || {}
510
+ weights.keys.inject({}) do |hash, key|
511
+ if key.is_a?(Class)
512
+ name = ThinkingSphinx::Index.name_for(key)
513
+ hash["#{name}_core"] = weights[key]
514
+ hash["#{name}_delta"] = weights[key]
515
+ else
516
+ hash[key] = weights[key]
517
+ end
518
+
519
+ hash
520
+ end
521
+ end
522
+
523
+ def group_by
524
+ options[:group] ? options[:group].to_s : nil
525
+ end
526
+
527
+ def group_function
528
+ options[:group] ? :attr : nil
529
+ end
530
+
531
+ def internal_filters
532
+ filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
533
+
534
+ class_crcs = classes.collect { |klass|
535
+ klass.to_crc32s
536
+ }.flatten
537
+
538
+ unless class_crcs.empty?
539
+ filters << Riddle::Client::Filter.new('class_crc', class_crcs)
540
+ end
541
+
542
+ filters << Riddle::Client::Filter.new(
543
+ 'sphinx_internal_id', filter_value(options[:without_ids]), true
544
+ ) if options[:without_ids]
545
+
546
+ filters
547
+ end
548
+
549
+ def condition_filters
550
+ (options[:conditions] || {}).collect { |attrib, value|
551
+ if attributes.include?(attrib.to_sym)
552
+ puts <<-MSG
553
+ Deprecation Warning: filters on attributes should be done using the :with
554
+ option, not :conditions. For example:
555
+ :with => {:#{attrib} => #{value.inspect}}
556
+ MSG
557
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
558
+ else
559
+ nil
560
+ end
561
+ }.compact
562
+ end
563
+
564
+ def filters
565
+ internal_filters +
566
+ condition_filters +
567
+ (options[:with] || {}).collect { |attrib, value|
568
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
569
+ } +
570
+ (options[:without] || {}).collect { |attrib, value|
571
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
572
+ } +
573
+ (options[:with_all] || {}).collect { |attrib, values|
574
+ Array(values).collect { |value|
575
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
576
+ }
577
+ }.flatten
578
+ end
579
+
580
+ # When passed a Time instance, returns the integer timestamp.
581
+ #
582
+ # If using Rails 2.1+, need to handle timezones to translate them back to
583
+ # UTC, as that's what datetimes will be stored as by MySQL.
584
+ #
585
+ # in_time_zone is a method that was added for the timezone support in
586
+ # Rails 2.1, which is why it's used for testing. I'm sure there's better
587
+ # ways, but this does the job.
588
+ #
589
+ def filter_value(value)
590
+ case value
591
+ when Range
592
+ filter_value(value.first).first..filter_value(value.last).first
593
+ when Array
594
+ value.collect { |v| filter_value(v) }.flatten
595
+ when Time
596
+ value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
597
+ when NilClass
598
+ 0
599
+ else
600
+ Array(value)
601
+ end
602
+ end
603
+
604
+ def anchor
605
+ return {} unless options[:geo] || (options[:lat] && options[:lng])
606
+
607
+ {
608
+ :latitude => options[:geo] ? options[:geo].first : options[:lat],
609
+ :longitude => options[:geo] ? options[:geo].last : options[:lng],
610
+ :latitude_attribute => latitude_attr.to_s,
611
+ :longitude_attribute => longitude_attr.to_s
612
+ }
613
+ end
614
+
615
+ def latitude_attr
616
+ options[:latitude_attr] ||
617
+ index_option(:latitude_attr) ||
618
+ attribute(:lat, :latitude)
619
+ end
620
+
621
+ def longitude_attr
622
+ options[:longitude_attr] ||
623
+ index_option(:longitude_attr) ||
624
+ attribute(:lon, :lng, :longitude)
625
+ end
626
+
627
+ def index_option(key)
628
+ return nil unless one_class
629
+
630
+ one_class.sphinx_indexes.collect { |index|
631
+ index.local_options[key]
632
+ }.compact.first
633
+ end
634
+
635
+ def attribute(*keys)
636
+ return nil unless one_class
637
+
638
+ keys.detect { |key|
639
+ attributes.include?(key)
640
+ }
641
+ end
642
+
643
+ def attributes
644
+ return [] unless one_class
645
+
646
+ attributes = one_class.sphinx_indexes.collect { |index|
647
+ index.attributes.collect { |attrib| attrib.unique_name }
648
+ }.flatten
649
+ end
650
+
651
+ def stale_retries
652
+ case options[:retry_stale]
653
+ when TrueClass
654
+ 3
655
+ when nil, FalseClass
656
+ 0
657
+ else
658
+ options[:retry_stale].to_i
659
+ end
660
+ end
661
+
662
+ def instances_from_class(klass, matches)
663
+ index_options = klass.sphinx_index_options
664
+
665
+ ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
666
+ instances = ids.length > 0 ? klass.find(
667
+ :all,
668
+ :joins => options[:joins],
669
+ :conditions => {klass.primary_key_for_sphinx.to_sym => ids},
670
+ :include => (options[:include] || index_options[:include]),
671
+ :select => (options[:select] || index_options[:select]),
672
+ :order => (options[:sql_order] || index_options[:sql_order] || "FIELD(id, #{ids.join(',') })")
673
+ ) : []
674
+
675
+ # Raise an exception if we find records in Sphinx but not in the DB, so
676
+ # the search method can retry without them. See
677
+ # ThinkingSphinx::Search.retry_search_on_stale_index.
678
+ if options[:raise_on_stale] && instances.length < ids.length
679
+ stale_ids = ids - instances.map { |i| i.id }
680
+ raise StaleIdsException, stale_ids
681
+ end
682
+
683
+ # if the user has specified an SQL order, return the collection
684
+ # without rearranging it into the Sphinx order
685
+ return instances
686
+ end
687
+
688
+ # Group results by class and call #find(:all) once for each group to reduce
689
+ # the number of #find's in multi-model searches.
690
+ #
691
+ def instances_from_matches
692
+ return single_class_results if one_class
693
+
694
+ groups = results[:matches].group_by { |match|
695
+ match[:attributes]["class_crc"]
696
+ }
697
+ groups.each do |crc, group|
698
+ group.replace(
699
+ instances_from_class(class_from_crc(crc), group)
700
+ )
701
+ end
702
+
703
+ results[:matches].collect do |match|
704
+ groups.detect { |crc, group|
705
+ crc == match[:attributes]["class_crc"]
706
+ }[1].compact.detect { |obj|
707
+ obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
708
+ }
709
+ end
710
+ end
711
+
712
+ def single_class_results
713
+ instances_from_class one_class, results[:matches]
714
+ end
715
+
716
+ def class_from_crc(crc)
717
+ config.models_by_crc[crc].constantize
718
+ end
719
+
720
+ def each_with_attribute(attribute, &block)
721
+ populate
722
+ results[:matches].each_with_index do |match, index|
723
+ yield self[index],
724
+ (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
725
+ end
726
+ end
727
+
728
+ def is_scope?(method)
729
+ one_class && one_class.sphinx_scopes.include?(method)
730
+ end
731
+
732
+ # Adds the default_sphinx_scope if set.
733
+ def add_default_scope
734
+ add_scope(one_class.get_default_sphinx_scope) if one_class && one_class.has_default_sphinx_scope?
735
+ end
736
+
737
+ def add_scope(method, *args, &block)
738
+ merge_search one_class.send(method, *args, &block)
739
+ end
740
+
741
+ def merge_search(search)
742
+ search.args.each { |arg| args << arg }
743
+
744
+ search.options.keys.each do |key|
745
+ if HashOptions.include?(key)
746
+ options[key] ||= {}
747
+ options[key].merge! search.options[key]
748
+ elsif ArrayOptions.include?(key)
749
+ options[key] ||= []
750
+ options[key] += search.options[key]
751
+ options[key].uniq!
752
+ else
753
+ options[key] = search.options[key]
754
+ end
755
+ end
756
+ end
757
+
758
+ def scoped_count
759
+ return self.total_entries if @options[:ids_only]
760
+
761
+ @options[:ids_only] = true
762
+ results_count = self.total_entries
763
+ @options[:ids_only] = false
764
+ @populated = false
765
+
766
+ results_count
767
+ end
768
+ end
769
+ end