thinking-sphinx 2.0.6 → 2.0.7

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