friendlyfashion-thinking-sphinx 2.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. data/HISTORY +244 -0
  2. data/LICENCE +20 -0
  3. data/README.textile +235 -0
  4. data/features/abstract_inheritance.feature +10 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +77 -0
  8. data/features/deleting_instances.feature +67 -0
  9. data/features/direct_attributes.feature +11 -0
  10. data/features/excerpts.feature +21 -0
  11. data/features/extensible_delta_indexing.feature +9 -0
  12. data/features/facets.feature +88 -0
  13. data/features/facets_across_model.feature +29 -0
  14. data/features/field_sorting.feature +18 -0
  15. data/features/handling_edits.feature +94 -0
  16. data/features/retry_stale_indexes.feature +24 -0
  17. data/features/searching_across_models.feature +20 -0
  18. data/features/searching_by_index.feature +40 -0
  19. data/features/searching_by_model.feature +175 -0
  20. data/features/searching_with_find_arguments.feature +56 -0
  21. data/features/sphinx_detection.feature +25 -0
  22. data/features/sphinx_scopes.feature +68 -0
  23. data/features/step_definitions/alpha_steps.rb +16 -0
  24. data/features/step_definitions/beta_steps.rb +7 -0
  25. data/features/step_definitions/common_steps.rb +201 -0
  26. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  27. data/features/step_definitions/facet_steps.rb +96 -0
  28. data/features/step_definitions/find_arguments_steps.rb +36 -0
  29. data/features/step_definitions/gamma_steps.rb +15 -0
  30. data/features/step_definitions/scope_steps.rb +19 -0
  31. data/features/step_definitions/search_steps.rb +94 -0
  32. data/features/step_definitions/sphinx_steps.rb +35 -0
  33. data/features/sti_searching.feature +19 -0
  34. data/features/support/env.rb +27 -0
  35. data/features/support/lib/generic_delta_handler.rb +8 -0
  36. data/features/thinking_sphinx/database.example.yml +3 -0
  37. data/features/thinking_sphinx/db/.gitignore +1 -0
  38. data/features/thinking_sphinx/db/fixtures/alphas.rb +8 -0
  39. data/features/thinking_sphinx/db/fixtures/authors.rb +1 -0
  40. data/features/thinking_sphinx/db/fixtures/betas.rb +11 -0
  41. data/features/thinking_sphinx/db/fixtures/boxes.rb +9 -0
  42. data/features/thinking_sphinx/db/fixtures/categories.rb +1 -0
  43. data/features/thinking_sphinx/db/fixtures/cats.rb +3 -0
  44. data/features/thinking_sphinx/db/fixtures/comments.rb +24 -0
  45. data/features/thinking_sphinx/db/fixtures/developers.rb +31 -0
  46. data/features/thinking_sphinx/db/fixtures/dogs.rb +3 -0
  47. data/features/thinking_sphinx/db/fixtures/extensible_betas.rb +10 -0
  48. data/features/thinking_sphinx/db/fixtures/foxes.rb +3 -0
  49. data/features/thinking_sphinx/db/fixtures/gammas.rb +10 -0
  50. data/features/thinking_sphinx/db/fixtures/music.rb +4 -0
  51. data/features/thinking_sphinx/db/fixtures/people.rb +1001 -0
  52. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  53. data/features/thinking_sphinx/db/fixtures/posts.rb +10 -0
  54. data/features/thinking_sphinx/db/fixtures/robots.rb +8 -0
  55. data/features/thinking_sphinx/db/fixtures/tags.rb +27 -0
  56. data/features/thinking_sphinx/db/migrations/create_alphas.rb +8 -0
  57. data/features/thinking_sphinx/db/migrations/create_animals.rb +5 -0
  58. data/features/thinking_sphinx/db/migrations/create_authors.rb +3 -0
  59. data/features/thinking_sphinx/db/migrations/create_authors_posts.rb +6 -0
  60. data/features/thinking_sphinx/db/migrations/create_betas.rb +5 -0
  61. data/features/thinking_sphinx/db/migrations/create_boxes.rb +5 -0
  62. data/features/thinking_sphinx/db/migrations/create_categories.rb +3 -0
  63. data/features/thinking_sphinx/db/migrations/create_comments.rb +10 -0
  64. data/features/thinking_sphinx/db/migrations/create_developers.rb +7 -0
  65. data/features/thinking_sphinx/db/migrations/create_extensible_betas.rb +5 -0
  66. data/features/thinking_sphinx/db/migrations/create_gammas.rb +3 -0
  67. data/features/thinking_sphinx/db/migrations/create_genres.rb +3 -0
  68. data/features/thinking_sphinx/db/migrations/create_music.rb +6 -0
  69. data/features/thinking_sphinx/db/migrations/create_people.rb +13 -0
  70. data/features/thinking_sphinx/db/migrations/create_posts.rb +6 -0
  71. data/features/thinking_sphinx/db/migrations/create_robots.rb +4 -0
  72. data/features/thinking_sphinx/db/migrations/create_taggings.rb +5 -0
  73. data/features/thinking_sphinx/db/migrations/create_tags.rb +4 -0
  74. data/features/thinking_sphinx/models/alpha.rb +23 -0
  75. data/features/thinking_sphinx/models/andrew.rb +17 -0
  76. data/features/thinking_sphinx/models/animal.rb +5 -0
  77. data/features/thinking_sphinx/models/author.rb +3 -0
  78. data/features/thinking_sphinx/models/beta.rb +13 -0
  79. data/features/thinking_sphinx/models/box.rb +8 -0
  80. data/features/thinking_sphinx/models/cat.rb +3 -0
  81. data/features/thinking_sphinx/models/category.rb +4 -0
  82. data/features/thinking_sphinx/models/comment.rb +10 -0
  83. data/features/thinking_sphinx/models/developer.rb +21 -0
  84. data/features/thinking_sphinx/models/dog.rb +3 -0
  85. data/features/thinking_sphinx/models/extensible_beta.rb +9 -0
  86. data/features/thinking_sphinx/models/fox.rb +5 -0
  87. data/features/thinking_sphinx/models/gamma.rb +5 -0
  88. data/features/thinking_sphinx/models/genre.rb +3 -0
  89. data/features/thinking_sphinx/models/medium.rb +5 -0
  90. data/features/thinking_sphinx/models/music.rb +10 -0
  91. data/features/thinking_sphinx/models/person.rb +24 -0
  92. data/features/thinking_sphinx/models/post.rb +22 -0
  93. data/features/thinking_sphinx/models/robot.rb +12 -0
  94. data/features/thinking_sphinx/models/tag.rb +3 -0
  95. data/features/thinking_sphinx/models/tagging.rb +4 -0
  96. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  97. data/lib/cucumber/thinking_sphinx/internal_world.rb +137 -0
  98. data/lib/cucumber/thinking_sphinx/sql_logger.rb +28 -0
  99. data/lib/thinking-sphinx.rb +1 -0
  100. data/lib/thinking_sphinx/action_controller.rb +31 -0
  101. data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
  102. data/lib/thinking_sphinx/active_record/collection_proxy.rb +47 -0
  103. data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
  104. data/lib/thinking_sphinx/active_record/delta.rb +67 -0
  105. data/lib/thinking_sphinx/active_record/has_many_association.rb +44 -0
  106. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
  107. data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
  108. data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
  109. data/lib/thinking_sphinx/active_record.rb +386 -0
  110. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
  111. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
  112. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +188 -0
  113. data/lib/thinking_sphinx/association.rb +230 -0
  114. data/lib/thinking_sphinx/attribute.rb +405 -0
  115. data/lib/thinking_sphinx/auto_version.rb +40 -0
  116. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  117. data/lib/thinking_sphinx/class_facet.rb +20 -0
  118. data/lib/thinking_sphinx/configuration.rb +375 -0
  119. data/lib/thinking_sphinx/context.rb +76 -0
  120. data/lib/thinking_sphinx/core/string.rb +15 -0
  121. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  122. data/lib/thinking_sphinx/deltas.rb +28 -0
  123. data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
  124. data/lib/thinking_sphinx/excerpter.rb +23 -0
  125. data/lib/thinking_sphinx/facet.rb +135 -0
  126. data/lib/thinking_sphinx/facet_search.rb +170 -0
  127. data/lib/thinking_sphinx/field.rb +98 -0
  128. data/lib/thinking_sphinx/index/builder.rb +315 -0
  129. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  130. data/lib/thinking_sphinx/index.rb +159 -0
  131. data/lib/thinking_sphinx/join.rb +37 -0
  132. data/lib/thinking_sphinx/property.rb +187 -0
  133. data/lib/thinking_sphinx/railtie.rb +43 -0
  134. data/lib/thinking_sphinx/search.rb +1061 -0
  135. data/lib/thinking_sphinx/search_methods.rb +439 -0
  136. data/lib/thinking_sphinx/sinatra.rb +7 -0
  137. data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
  138. data/lib/thinking_sphinx/source/sql.rb +174 -0
  139. data/lib/thinking_sphinx/source.rb +194 -0
  140. data/lib/thinking_sphinx/tasks.rb +142 -0
  141. data/lib/thinking_sphinx/test.rb +55 -0
  142. data/lib/thinking_sphinx/version.rb +3 -0
  143. data/lib/thinking_sphinx.rb +297 -0
  144. data/spec/fixtures/data.sql +32 -0
  145. data/spec/fixtures/database.yml.default +3 -0
  146. data/spec/fixtures/models.rb +164 -0
  147. data/spec/fixtures/structure.sql +146 -0
  148. data/spec/spec_helper.rb +61 -0
  149. data/spec/sphinx_helper.rb +60 -0
  150. data/spec/support/rails.rb +25 -0
  151. data/spec/thinking_sphinx/active_record/delta_spec.rb +122 -0
  152. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +173 -0
  153. data/spec/thinking_sphinx/active_record/scopes_spec.rb +176 -0
  154. data/spec/thinking_sphinx/active_record_spec.rb +573 -0
  155. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +145 -0
  156. data/spec/thinking_sphinx/association_spec.rb +250 -0
  157. data/spec/thinking_sphinx/attribute_spec.rb +552 -0
  158. data/spec/thinking_sphinx/auto_version_spec.rb +103 -0
  159. data/spec/thinking_sphinx/configuration_spec.rb +326 -0
  160. data/spec/thinking_sphinx/context_spec.rb +126 -0
  161. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  162. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  163. data/spec/thinking_sphinx/excerpter_spec.rb +49 -0
  164. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  165. data/spec/thinking_sphinx/facet_spec.rb +359 -0
  166. data/spec/thinking_sphinx/field_spec.rb +127 -0
  167. data/spec/thinking_sphinx/index/builder_spec.rb +532 -0
  168. data/spec/thinking_sphinx/index/faux_column_spec.rb +36 -0
  169. data/spec/thinking_sphinx/index_spec.rb +189 -0
  170. data/spec/thinking_sphinx/search_methods_spec.rb +156 -0
  171. data/spec/thinking_sphinx/search_spec.rb +1455 -0
  172. data/spec/thinking_sphinx/source_spec.rb +267 -0
  173. data/spec/thinking_sphinx/test_spec.rb +20 -0
  174. data/spec/thinking_sphinx_spec.rb +204 -0
  175. metadata +524 -0
@@ -0,0 +1,1061 @@
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 < Array
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?
15
+ respond_to_missing? send should tap 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, :without_any]
26
+ ArrayOptions = [:classes, :without_ids]
27
+
28
+ attr_reader :args, :options
29
+
30
+ # Deprecated. Use ThinkingSphinx.search
31
+ def self.search(*args)
32
+ warn '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
+ warn '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
+ warn '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
+ warn '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
+ warn 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
57
+ ThinkingSphinx.facets(*args)
58
+ end
59
+
60
+ def self.warn(message)
61
+ ::ActiveSupport::Deprecation.warn message
62
+ end
63
+
64
+ def self.bundle_searches(enum = nil)
65
+ bundle = ThinkingSphinx::BundledSearch.new
66
+
67
+ if enum.nil?
68
+ yield bundle
69
+ else
70
+ enum.each { |item| yield bundle, item }
71
+ end
72
+
73
+ bundle.searches
74
+ end
75
+
76
+ def self.matching_fields(fields, bitmask)
77
+ matches = []
78
+ bitstring = bitmask.to_s(2).rjust(32, '0').reverse
79
+
80
+ fields.each_with_index do |field, index|
81
+ matches << field if bitstring[index, 1] == '1'
82
+ end
83
+ matches
84
+ end
85
+
86
+ def initialize(*args)
87
+ ThinkingSphinx.context.define_indexes
88
+
89
+ @array = []
90
+ @options = args.extract_options!
91
+ @args = args
92
+
93
+ add_default_scope unless options[:ignore_default]
94
+
95
+ populate if @options[:populate]
96
+ end
97
+
98
+ def ==(object)
99
+ populate
100
+ super
101
+ end
102
+
103
+ def to_a
104
+ populate
105
+ @array
106
+ end
107
+
108
+ # Populates the search result set
109
+ def all
110
+ populate
111
+ self
112
+ end
113
+
114
+ def freeze
115
+ populate
116
+ @array.freeze
117
+ self
118
+ end
119
+
120
+ def as_json(*args)
121
+ populate
122
+ @array.as_json(*args)
123
+ end
124
+
125
+ # Indication of whether the request has been made to Sphinx for the search
126
+ # query.
127
+ #
128
+ # @return [Boolean] true if the results have been requested.
129
+ #
130
+ def populated?
131
+ !!@populated
132
+ end
133
+
134
+ # Indication of whether the request resulted in an error from Sphinx.
135
+ #
136
+ # @return [Boolean] true if Sphinx reports query error
137
+ #
138
+ def error?
139
+ !!error
140
+ end
141
+
142
+ # The Sphinx-reported error, if any.
143
+ #
144
+ # @return [String, nil]
145
+ #
146
+ def error
147
+ populate
148
+ @results[:error]
149
+ end
150
+
151
+ # Indication of whether the request resulted in a warning from Sphinx.
152
+ #
153
+ # @return [Boolean] true if Sphinx reports query warning
154
+ #
155
+ def warning?
156
+ !!warning
157
+ end
158
+
159
+ # The Sphinx-reported warning, if any.
160
+ #
161
+ # @return [String, nil]
162
+ #
163
+ def warning
164
+ populate
165
+ @results[:warning]
166
+ end
167
+
168
+ # The query result hash from Riddle.
169
+ #
170
+ # @return [Hash] Raw Sphinx results
171
+ #
172
+ def results
173
+ populate
174
+ @results
175
+ end
176
+
177
+ def method_missing(method, *args, &block)
178
+ if is_scope?(method)
179
+ add_scope(method, *args, &block)
180
+ return self
181
+ elsif method == :search_count
182
+ merge_search one_class.search(*args), self.args, options
183
+ return scoped_count
184
+ elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
185
+ super
186
+ elsif !SafeMethods.include?(method.to_s)
187
+ populate
188
+ end
189
+
190
+ if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
191
+ each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
192
+ else
193
+ @array.send(method, *args, &block)
194
+ end
195
+ end
196
+
197
+ # Returns true if the Search object or the underlying Array object respond
198
+ # to the requested method.
199
+ #
200
+ # @param [Symbol] method The method name
201
+ # @return [Boolean] true if either Search or Array responds to the method.
202
+ #
203
+ def respond_to?(method, include_private = false)
204
+ super || @array.respond_to?(method, include_private)
205
+ end
206
+
207
+ # The current page number of the result set. Defaults to 1 if no page was
208
+ # explicitly requested.
209
+ #
210
+ # @return [Integer]
211
+ #
212
+ def current_page
213
+ @options[:page].blank? ? 1 : @options[:page].to_i
214
+ end
215
+
216
+ def first_page?
217
+ current_page == 1
218
+ end
219
+
220
+ # Kaminari support
221
+ def page(page_number)
222
+ @options[:page] = page_number
223
+ self
224
+ end
225
+
226
+ # The next page number of the result set. If there are no more pages
227
+ # available, nil is returned.
228
+ #
229
+ # @return [Integer, nil]
230
+ #
231
+ def next_page
232
+ current_page >= total_pages ? nil : current_page + 1
233
+ end
234
+
235
+ def next_page?
236
+ !next_page.nil?
237
+ end
238
+
239
+ def last_page?
240
+ next_page.nil?
241
+ end
242
+
243
+ # The previous page number of the result set. If this is the first page,
244
+ # then nil is returned.
245
+ #
246
+ # @return [Integer, nil]
247
+ #
248
+ def previous_page
249
+ current_page == 1 ? nil : current_page - 1
250
+ end
251
+
252
+ # The amount of records per set of paged results. Defaults to 20 unless a
253
+ # specific page size is requested.
254
+ #
255
+ # @return [Integer]
256
+ #
257
+ def per_page
258
+ @options[:limit] ||= @options[:per_page]
259
+ @options[:limit] ||= 20
260
+ @options[:limit].to_i
261
+ end
262
+ # Kaminari support
263
+ alias_method :limit_value, :per_page
264
+
265
+ # Kaminari support
266
+ def per(limit)
267
+ @options[:limit] = limit
268
+ self
269
+ end
270
+
271
+ # The total number of pages available if the results are paginated.
272
+ #
273
+ # @return [Integer]
274
+ #
275
+ def total_pages
276
+ populate
277
+ return 0 if @results.nil? || @results[:total].nil?
278
+
279
+ @total_pages ||= (@results[:total] / per_page.to_f).ceil
280
+ end
281
+ # Compatibility with kaminari and older versions of will_paginate
282
+ alias_method :page_count, :total_pages
283
+ alias_method :num_pages, :total_pages
284
+
285
+ # Query time taken
286
+ #
287
+ # @return [Integer]
288
+ #
289
+ def query_time
290
+ populate
291
+ return 0 if @results[:time].nil?
292
+
293
+ @query_time ||= @results[:time]
294
+ end
295
+
296
+ # The total number of search results available.
297
+ #
298
+ # @return [Integer]
299
+ #
300
+ def total_entries
301
+ populate
302
+ return 0 if @results.nil? || @results[:total_found].nil?
303
+
304
+ @total_entries ||= @results[:total_found]
305
+ end
306
+
307
+ # Compatibility with kaminari
308
+ alias_method :total_count, :total_entries
309
+
310
+ # The current page's offset, based on the number of records per page.
311
+ # Or explicit :offset if given.
312
+ #
313
+ # @return [Integer]
314
+ #
315
+ def offset
316
+ @options[:offset] || ((current_page - 1) * per_page)
317
+ end
318
+
319
+ alias_method :offset_value, :offset
320
+
321
+ def indexes
322
+ return options[:index] if options[:index]
323
+ return '*' if classes.empty?
324
+
325
+ classes.collect { |klass|
326
+ klass.sphinx_index_names
327
+ }.flatten.uniq.join(',')
328
+ end
329
+
330
+ def each_with_groupby_and_count(&block)
331
+ populate
332
+ results[:matches].each_with_index do |match, index|
333
+ yield self[index],
334
+ match[:attributes]["@groupby"],
335
+ match[:attributes]["@count"]
336
+ end
337
+ end
338
+ alias_method :each_with_group_and_count, :each_with_groupby_and_count
339
+
340
+ def each_with_weighting(&block)
341
+ populate
342
+ results[:matches].each_with_index do |match, index|
343
+ yield self[index], match[:weight]
344
+ end
345
+ end
346
+
347
+ def each_with_match(&block)
348
+ populate
349
+ results[:matches].each_with_index do |match, index|
350
+ yield self[index], match
351
+ end
352
+ end
353
+
354
+ def excerpt_for(string, model = nil)
355
+ if model.nil? && one_class
356
+ model ||= one_class
357
+ end
358
+
359
+ populate
360
+
361
+ index = options[:index] || "#{model.core_index_names.first}"
362
+ client.excerpts(
363
+ {
364
+ :docs => [string.to_s],
365
+ :words => query,
366
+ :index => index.split(',').first.strip
367
+ }.merge(options[:excerpt_options] || {})
368
+ ).first
369
+ end
370
+
371
+ def search(*args)
372
+ args << args.extract_options!.merge(:ignore_default => true)
373
+ merge_search ThinkingSphinx::Search.new(*args), self.args, options
374
+ self
375
+ end
376
+
377
+ def search_for_ids(*args)
378
+ args << args.extract_options!.merge(
379
+ :ignore_default => true,
380
+ :ids_only => true
381
+ )
382
+ merge_search ThinkingSphinx::Search.new(*args), self.args, options
383
+ self
384
+ end
385
+
386
+ def facets(*args)
387
+ options = args.extract_options!
388
+ merge_search self, args, options
389
+ args << options
390
+
391
+ ThinkingSphinx::FacetSearch.new(*args)
392
+ end
393
+
394
+ def client
395
+ client = options[:client] || config.client
396
+
397
+ prepare client
398
+ end
399
+
400
+ def append_to(client)
401
+ prepare client
402
+ client.append_query query, indexes, comment
403
+ client.reset
404
+ end
405
+
406
+ def populate_from_queue(results)
407
+ return if @populated
408
+ @populated = true
409
+ @results = results
410
+
411
+ compose_results
412
+ end
413
+
414
+ private
415
+
416
+ def config
417
+ ThinkingSphinx::Configuration.instance
418
+ end
419
+
420
+ def populate
421
+ return if @populated
422
+ @populated = true
423
+ retries = hard_retries
424
+
425
+ begin
426
+ retry_on_index_not_preread do
427
+ retry_on_stale_index do
428
+ begin
429
+ log query do
430
+ @results = client.query query, indexes, comment
431
+ end
432
+ total = @results[:total_found].to_i
433
+ log "Found #{total} result#{'s' unless total == 1}"
434
+
435
+ log "Sphinx Daemon returned warning: #{warning}" if warning?
436
+
437
+ if error?
438
+ log "Sphinx Daemon returned error: #{error}"
439
+ raise SphinxError.new(error, @results) unless options[:ignore_errors]
440
+ end
441
+ rescue Errno::ECONNREFUSED => err
442
+ raise ThinkingSphinx::ConnectionError,
443
+ 'Connection to Sphinx Daemon (searchd) failed.'
444
+ end
445
+
446
+ compose_results
447
+ end
448
+ end
449
+ rescue => e
450
+ log 'Caught Sphinx exception: %s (%s %s left)' % [
451
+ e.message, retries, (retries == 1 ? 'try' : 'tries')
452
+ ]
453
+ retries -= 1
454
+ if retries >= 0
455
+ retry
456
+ else
457
+ raise e
458
+ end
459
+ end
460
+ end
461
+
462
+ def compose_results
463
+ if options[:ids_only]
464
+ compose_ids_results
465
+ elsif options[:attributes_only]
466
+ compose_attributes_results
467
+ elsif options[:only]
468
+ compose_only_results
469
+ else
470
+ replace instances_from_matches
471
+ add_excerpter
472
+ add_sphinx_attributes
473
+ add_matching_fields if client.rank_mode == :fieldmask
474
+ end
475
+ end
476
+
477
+ def compose_ids_results
478
+ replace @results[:matches].collect { |match|
479
+ match[:attributes]['sphinx_internal_id']
480
+ }
481
+ end
482
+
483
+ def compose_attributes_results
484
+ replace @results[:matches].collect { |match|
485
+ attributes = {}
486
+ match[:attributes].each do |name, value|
487
+ attributes[name.to_sym] = match[:attributes][name]
488
+ end
489
+ attributes
490
+ }
491
+ end
492
+
493
+ def compose_only_results
494
+ replace @results[:matches].collect { |match|
495
+ case only = options[:only]
496
+ when String, Symbol
497
+ match[:attributes][only.to_s]
498
+ when Array
499
+ only.inject({}) do |hash, attribute|
500
+ hash[attribute.to_sym] = match[:attributes][attribute.to_s]
501
+ hash
502
+ end
503
+ else
504
+ raise "Unexpected object for :only argument. String or Array is expected, #{only.class} was received."
505
+ end
506
+ }
507
+ end
508
+
509
+ def add_excerpter
510
+ each do |object|
511
+ next if object.nil?
512
+
513
+ object.excerpts = ThinkingSphinx::Excerpter.new self, object
514
+ end
515
+ end
516
+
517
+ def add_sphinx_attributes
518
+ each do |object|
519
+ next if object.nil?
520
+
521
+ match = match_hash object
522
+ next if match.nil?
523
+
524
+ object.sphinx_attributes = match[:attributes]
525
+ end
526
+ end
527
+
528
+ def add_matching_fields
529
+ each do |object|
530
+ next if object.nil?
531
+
532
+ match = match_hash object
533
+ next if match.nil?
534
+ object.matching_fields = ThinkingSphinx::Search.matching_fields(
535
+ @results[:fields], match[:weight]
536
+ )
537
+ end
538
+ end
539
+
540
+ def match_hash(object)
541
+ @results[:matches].detect { |match|
542
+ class_crc = object.class.name
543
+ class_crc = object.class.to_crc32 if Riddle.loaded_version.to_i < 2
544
+
545
+ match[:attributes]['sphinx_internal_id'] == object.
546
+ primary_key_for_sphinx &&
547
+ match[:attributes][crc_attribute] == class_crc
548
+ }
549
+ end
550
+
551
+ def self.log(message, &block)
552
+ if ThinkingSphinx::ActiveRecord::LogSubscriber.logger.nil?
553
+ yield if block_given?
554
+ return
555
+ end
556
+
557
+ if block_given?
558
+ ::ActiveSupport::Notifications.
559
+ instrument('query.thinking_sphinx', :query => message, &block)
560
+ else
561
+ ::ActiveSupport::Notifications.
562
+ instrument('message.thinking_sphinx', :message => message)
563
+ end
564
+ end
565
+
566
+ def log(query, &block)
567
+ self.class.log(query, &block)
568
+ end
569
+
570
+ def prepare(client)
571
+ index_options = {}
572
+ if one_class && one_class.sphinx_indexes && one_class.sphinx_indexes.first
573
+ index_options = one_class.sphinx_indexes.first.local_options
574
+ end
575
+
576
+ [
577
+ :max_matches, :group_by, :group_function, :group_clause,
578
+ :group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
579
+ :rank_mode, :rank_expr, :max_query_time, :field_weights
580
+ ].each do |key|
581
+ value = options[key] || index_options[key]
582
+ client.send("#{key}=", value) if value
583
+ end
584
+
585
+ # treated non-standard as :select is already used for AR queries
586
+ client.select = options[:sphinx_select] || '*'
587
+
588
+ client.limit = per_page
589
+ client.offset = offset
590
+ client.match_mode = match_mode
591
+ client.filters = filters
592
+ client.sort_mode = sort_mode
593
+ client.sort_by = sort_by
594
+ client.group_by = group_by if group_by
595
+ client.group_function = group_function if group_function
596
+ client.index_weights = index_weights
597
+ client.anchor = anchor
598
+
599
+ client
600
+ end
601
+
602
+ def retry_on_stale_index(&block)
603
+ stale_ids = []
604
+ retries = stale_retries
605
+
606
+ begin
607
+ options[:raise_on_stale] = retries > 0
608
+ block.call
609
+
610
+ # If ThinkingSphinx::Search#instances_from_matches found records in
611
+ # Sphinx but not in the DB and the :raise_on_stale option is set, this
612
+ # exception is raised. We retry a limited number of times, excluding the
613
+ # stale ids from the search.
614
+ rescue StaleIdsException => err
615
+ retries -= 1
616
+
617
+ # For logging
618
+ stale_ids |= err.ids
619
+ # ID exclusion
620
+ options[:without_ids] = Array(options[:without_ids]) | err.ids
621
+
622
+ log 'Stale Ids (%s %s left): %s' % [
623
+ retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
624
+ ]
625
+ retry
626
+ end
627
+ end
628
+
629
+ def retry_on_index_not_preread(&block)
630
+ attempt = 0
631
+ begin
632
+ block.call
633
+ rescue ThinkingSphinx::SphinxError => e
634
+ raise unless e.message.include? "index not preread"
635
+
636
+ attempt += 1
637
+ if attempt > 0
638
+ raise
639
+ else
640
+ retry
641
+ end
642
+ end
643
+ end
644
+
645
+ def classes
646
+ @classes ||= options[:classes] || []
647
+ end
648
+
649
+ def one_class
650
+ @one_class ||= classes.length != 1 ? nil : classes.first
651
+ end
652
+
653
+ def query
654
+ @query ||= begin
655
+ q = @args.join(' ') << conditions_as_query
656
+ (options[:star] ? star_query(q) : q).strip
657
+ end
658
+ end
659
+
660
+ def conditions_as_query
661
+ return '' if @options[:conditions].blank?
662
+
663
+ ' ' + @options[:conditions].keys.collect { |key|
664
+ "@#{key} #{options[:conditions][key]}"
665
+ }.join(' ')
666
+ end
667
+
668
+ def star_query(query)
669
+ token = options[:star].is_a?(Regexp) ? options[:star] : default_star_token
670
+
671
+ query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
672
+ pre, proper, post = $`, $&, $'
673
+ # E.g. "@foo", "/2", "~3", but not as part of a token
674
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z}) ||
675
+ pre.match(%r{(\W|^)@\([^\)]*$})
676
+ # E.g. "foo bar", with quotes
677
+ is_quote = proper.starts_with?('"') && proper.ends_with?('"')
678
+ has_star = pre.ends_with?("*") || post.starts_with?("*")
679
+ if is_operator || is_quote || has_star
680
+ proper
681
+ else
682
+ "*#{proper}*"
683
+ end
684
+ end
685
+ end
686
+
687
+ if Regexp.instance_methods.include?(:encoding)
688
+ DefaultStarToken = Regexp.new('\p{Word}+')
689
+ else
690
+ DefaultStarToken = Regexp.new('\w+', nil, 'u')
691
+ end
692
+
693
+ def default_star_token
694
+ DefaultStarToken
695
+ end
696
+
697
+ def comment
698
+ options[:comment] || ''
699
+ end
700
+
701
+ def match_mode
702
+ options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
703
+ end
704
+
705
+ def sort_mode
706
+ @sort_mode ||= case options[:sort_mode]
707
+ when :asc
708
+ :attr_asc
709
+ when :desc
710
+ :attr_desc
711
+ when nil
712
+ case options[:order]
713
+ when String
714
+ :extended
715
+ when Symbol
716
+ :attr_asc
717
+ else
718
+ :relevance
719
+ end
720
+ else
721
+ options[:sort_mode]
722
+ end
723
+ end
724
+
725
+ def sort_by
726
+ case @sort_by = (options[:sort_by] || options[:order])
727
+ when String
728
+ sorted_fields_to_attributes(@sort_by.clone)
729
+ when Symbol
730
+ field_names.include?(@sort_by) ?
731
+ @sort_by.to_s.concat('_sort') : @sort_by.to_s
732
+ else
733
+ ''
734
+ end
735
+ end
736
+
737
+ def field_names
738
+ return [] unless one_class
739
+
740
+ one_class.sphinx_indexes.collect { |index|
741
+ index.fields.collect { |field| field.unique_name }
742
+ }.flatten
743
+ end
744
+
745
+ def sorted_fields_to_attributes(order_string)
746
+ field_names.each { |field|
747
+ order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
748
+ match.gsub field.to_s, field.to_s.concat("_sort")
749
+ }
750
+ }
751
+
752
+ order_string
753
+ end
754
+
755
+ # Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
756
+ # { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
757
+ #
758
+ def index_weights
759
+ weights = options[:index_weights] || {}
760
+ weights.keys.inject({}) do |hash, key|
761
+ if key.is_a?(Class)
762
+ name = ThinkingSphinx::Index.name_for(key)
763
+ hash["#{name}_core"] = weights[key]
764
+ hash["#{name}_delta"] = weights[key]
765
+ else
766
+ hash[key] = weights[key]
767
+ end
768
+
769
+ hash
770
+ end
771
+ end
772
+
773
+ def group_by
774
+ options[:group] ? options[:group].to_s : nil
775
+ end
776
+
777
+ def group_function
778
+ options[:group] ? :attr : nil
779
+ end
780
+
781
+ def internal_filters
782
+ filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
783
+
784
+ class_crcs = classes.collect { |klass|
785
+ klass.to_crc32s
786
+ }.flatten
787
+
788
+ unless class_crcs.empty?
789
+ filters << Riddle::Client::Filter.new('class_crc', class_crcs)
790
+ end
791
+
792
+ filters << Riddle::Client::Filter.new(
793
+ 'sphinx_internal_id', filter_value(options[:without_ids]), true
794
+ ) unless options[:without_ids].nil? || options[:without_ids].empty?
795
+
796
+ filters
797
+ end
798
+
799
+ def filters
800
+ internal_filters +
801
+ (options[:with] || {}).collect { |attrib, value|
802
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
803
+ } +
804
+ (options[:without] || {}).collect { |attrib, value|
805
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
806
+ } +
807
+ (options[:with_all] || {}).collect { |attrib, values|
808
+ Array(values).collect { |value|
809
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
810
+ }
811
+ }.flatten +
812
+ (options[:without_any] || {}).collect { |attrib, values|
813
+ Array(values).collect { |value|
814
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
815
+ }
816
+ }.flatten
817
+ end
818
+
819
+ # When passed a Time instance, returns the integer timestamp.
820
+ def filter_value(value)
821
+ case value
822
+ when Range
823
+ filter_value(value.first).first..filter_value(value.last).first
824
+ when Array
825
+ value.collect { |v| filter_value(v) }.flatten
826
+ when Time
827
+ [value.to_i]
828
+ when Date
829
+ [Time.utc(value.year, value.month, value.day).to_i]
830
+ when NilClass
831
+ 0
832
+ else
833
+ Array(value)
834
+ end
835
+ end
836
+
837
+ def anchor
838
+ return {} unless options[:geo] || (options[:lat] && options[:lng])
839
+
840
+ {
841
+ :latitude => options[:geo] ? options[:geo].first : options[:lat],
842
+ :longitude => options[:geo] ? options[:geo].last : options[:lng],
843
+ :latitude_attribute => latitude_attr.to_s,
844
+ :longitude_attribute => longitude_attr.to_s
845
+ }
846
+ end
847
+
848
+ def latitude_attr
849
+ options[:latitude_attr] ||
850
+ index_option(:latitude_attr) ||
851
+ attribute(:lat, :latitude)
852
+ end
853
+
854
+ def longitude_attr
855
+ options[:longitude_attr] ||
856
+ index_option(:longitude_attr) ||
857
+ attribute(:lon, :lng, :longitude)
858
+ end
859
+
860
+ def index_option(key)
861
+ return nil unless one_class
862
+
863
+ one_class.sphinx_indexes.collect { |index|
864
+ index.local_options[key]
865
+ }.compact.first
866
+ end
867
+
868
+ def attribute(*keys)
869
+ return nil unless one_class
870
+
871
+ keys.detect { |key|
872
+ attributes.include?(key)
873
+ }
874
+ end
875
+
876
+ def attributes
877
+ return [] unless one_class
878
+
879
+ attributes = one_class.sphinx_indexes.collect { |index|
880
+ index.attributes.collect { |attrib| attrib.unique_name }
881
+ }.flatten
882
+ end
883
+
884
+ def stale_retries
885
+ case options[:retry_stale]
886
+ when TrueClass
887
+ 3
888
+ when nil, FalseClass
889
+ 0
890
+ else
891
+ options[:retry_stale].to_i
892
+ end
893
+ end
894
+
895
+ def hard_retries
896
+ options[:hard_retry_count] || config.hard_retry_count
897
+ end
898
+
899
+ def include_for_class(klass)
900
+ includes = options[:include] || klass.sphinx_index_options[:include]
901
+
902
+ case includes
903
+ when NilClass
904
+ nil
905
+ when Array
906
+ include_from_array includes, klass
907
+ when Symbol
908
+ klass.reflections[includes].nil? ? nil : includes
909
+ when Hash
910
+ include_from_hash includes, klass
911
+ else
912
+ includes
913
+ end
914
+ end
915
+
916
+ def include_from_array(array, klass)
917
+ scoped_array = []
918
+ array.each do |value|
919
+ case value
920
+ when Hash
921
+ scoped_hash = include_from_hash(value, klass)
922
+ scoped_array << scoped_hash unless scoped_hash.nil?
923
+ else
924
+ scoped_array << value unless klass.reflections[value].nil?
925
+ end
926
+ end
927
+ scoped_array.empty? ? nil : scoped_array
928
+ end
929
+
930
+ def include_from_hash(hash, klass)
931
+ scoped_hash = {}
932
+ hash.keys.each do |key|
933
+ scoped_hash[key] = hash[key] unless klass.reflections[key].nil?
934
+ end
935
+ scoped_hash.empty? ? nil : scoped_hash
936
+ end
937
+
938
+ def instances_from_class(klass, matches)
939
+ index_options = klass.sphinx_index_options
940
+
941
+ ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
942
+ instances = ids.length > 0 ? klass.unscoped.find(
943
+ :all,
944
+ :joins => options[:joins],
945
+ :conditions => {klass.primary_key_for_sphinx.to_sym => ids},
946
+ :include => include_for_class(klass),
947
+ :select => (options[:select] || index_options[:select]),
948
+ :order => (options[:sql_order] || index_options[:sql_order])
949
+ ) : []
950
+
951
+ # Raise an exception if we find records in Sphinx but not in the DB, so
952
+ # the search method can retry without them. See
953
+ # ThinkingSphinx::Search.retry_search_on_stale_index.
954
+ if options[:raise_on_stale] && instances.length < ids.length
955
+ stale_ids = ids - instances.map { |i| i.id }
956
+ raise StaleIdsException, stale_ids
957
+ end
958
+
959
+ # if the user has specified an SQL order, return the collection
960
+ # without rearranging it into the Sphinx order
961
+ return instances if (options[:sql_order] || index_options[:sql_order])
962
+
963
+ ids.collect { |obj_id|
964
+ instances.detect do |obj|
965
+ obj.primary_key_for_sphinx == obj_id
966
+ end
967
+ }
968
+ end
969
+
970
+ # Group results by class and call #find(:all) once for each group to reduce
971
+ # the number of #find's in multi-model searches.
972
+ #
973
+ def instances_from_matches
974
+ return single_class_results if one_class
975
+
976
+ groups = results[:matches].group_by { |match|
977
+ match[:attributes][crc_attribute]
978
+ }
979
+ groups.each do |crc, group|
980
+ group.replace(
981
+ instances_from_class(class_from_crc(crc), group)
982
+ )
983
+ end
984
+
985
+ results[:matches].collect do |match|
986
+ groups.detect { |crc, group|
987
+ crc == match[:attributes][crc_attribute]
988
+ }[1].compact.detect { |obj|
989
+ obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
990
+ }
991
+ end
992
+ end
993
+
994
+ def single_class_results
995
+ instances_from_class one_class, results[:matches]
996
+ end
997
+
998
+ def class_from_crc(crc)
999
+ if Riddle.loaded_version.to_i < 2
1000
+ config.models_by_crc[crc].constantize
1001
+ else
1002
+ crc.constantize
1003
+ end
1004
+ end
1005
+
1006
+ def each_with_attribute(attribute, &block)
1007
+ populate
1008
+ results[:matches].each_with_index do |match, index|
1009
+ yield self[index],
1010
+ (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
1011
+ end
1012
+ end
1013
+
1014
+ def is_scope?(method)
1015
+ one_class && one_class.sphinx_scopes.include?(method)
1016
+ end
1017
+
1018
+ # Adds the default_sphinx_scope if set.
1019
+ def add_default_scope
1020
+ return unless one_class && one_class.has_default_sphinx_scope?
1021
+ add_scope(one_class.get_default_sphinx_scope.to_sym)
1022
+ end
1023
+
1024
+ def add_scope(method, *args, &block)
1025
+ method = "#{method}_without_default".to_sym
1026
+ merge_search one_class.send(method, *args, &block), self.args, options
1027
+ end
1028
+
1029
+ def merge_search(search, args, options)
1030
+ search.args.each { |arg| args << arg }
1031
+
1032
+ search.options.keys.each do |key|
1033
+ if HashOptions.include?(key)
1034
+ options[key] ||= {}
1035
+ options[key].merge! search.options[key]
1036
+ elsif ArrayOptions.include?(key)
1037
+ options[key] ||= []
1038
+ options[key] += search.options[key]
1039
+ options[key].uniq!
1040
+ else
1041
+ options[key] = search.options[key]
1042
+ end
1043
+ end
1044
+ end
1045
+
1046
+ def scoped_count
1047
+ return self.total_entries if(@options[:ids_only] || @options[:only])
1048
+
1049
+ @options[:ids_only] = true
1050
+ results_count = self.total_entries
1051
+ @options[:ids_only] = false
1052
+ @populated = false
1053
+
1054
+ results_count
1055
+ end
1056
+
1057
+ def crc_attribute
1058
+ Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
1059
+ end
1060
+ end
1061
+ end