thinking-sphinx 2.0.6 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
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