friendlyfashion-thinking-sphinx 2.0.13

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 (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