angelf-thinking-sphinx 1.3.18

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