thinking-sphinx 1.4.6 → 1.4.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 (39) hide show
  1. data/README.textile +6 -1
  2. data/features/searching_by_model.feature +24 -30
  3. data/features/thinking_sphinx/db/.gitignore +1 -0
  4. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  5. data/lib/cucumber/thinking_sphinx/internal_world.rb +26 -26
  6. data/lib/thinking_sphinx.rb +17 -26
  7. data/lib/thinking_sphinx/active_record.rb +69 -74
  8. data/lib/thinking_sphinx/active_record/attribute_updates.rb +11 -10
  9. data/lib/thinking_sphinx/active_record/has_many_association.rb +2 -1
  10. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +11 -11
  11. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +34 -20
  12. data/lib/thinking_sphinx/association.rb +12 -7
  13. data/lib/thinking_sphinx/attribute.rb +64 -61
  14. data/lib/thinking_sphinx/configuration.rb +32 -36
  15. data/lib/thinking_sphinx/context.rb +3 -2
  16. data/lib/thinking_sphinx/deploy/capistrano.rb +7 -9
  17. data/lib/thinking_sphinx/search.rb +201 -178
  18. data/lib/thinking_sphinx/source/sql.rb +1 -1
  19. data/lib/thinking_sphinx/tasks.rb +21 -19
  20. data/lib/thinking_sphinx/version.rb +3 -0
  21. data/spec/fixtures/data.sql +32 -0
  22. data/spec/fixtures/database.yml.default +3 -0
  23. data/spec/fixtures/models.rb +161 -0
  24. data/spec/fixtures/structure.sql +146 -0
  25. data/spec/spec_helper.rb +57 -0
  26. data/spec/sphinx_helper.rb +61 -0
  27. data/spec/thinking_sphinx/active_record/delta_spec.rb +24 -24
  28. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +22 -0
  29. data/spec/thinking_sphinx/active_record/scopes_spec.rb +25 -25
  30. data/spec/thinking_sphinx/active_record_spec.rb +110 -109
  31. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +38 -38
  32. data/spec/thinking_sphinx/association_spec.rb +20 -2
  33. data/spec/thinking_sphinx/context_spec.rb +61 -64
  34. data/spec/thinking_sphinx/search_spec.rb +7 -0
  35. data/spec/thinking_sphinx_spec.rb +47 -46
  36. metadata +50 -98
  37. data/VERSION +0 -1
  38. data/tasks/distribution.rb +0 -34
  39. data/tasks/testing.rb +0 -80
@@ -15,6 +15,7 @@ module ThinkingSphinx
15
15
  # address:: 127.0.0.1
16
16
  # port:: 9312
17
17
  # allow star:: false
18
+ # stop timeout:: 5
18
19
  # min prefix length:: 1
19
20
  # min infix length:: 1
20
21
  # mem limit:: 64M
@@ -49,29 +50,25 @@ module ThinkingSphinx
49
50
  class Configuration
50
51
  include Singleton
51
52
 
52
- SourceOptions = %w( mysql_connect_flags mysql_ssl_cert mysql_ssl_key
53
- mysql_ssl_ca sql_range_step sql_query_pre sql_query_post
54
- sql_query_killlist sql_ranged_throttle sql_query_post_index unpack_zlib
55
- unpack_mysqlcompress unpack_mysqlcompress_maxsize )
56
-
57
- IndexOptions = %w( blend_chars charset_table charset_type charset_dictpath
58
- docinfo enable_star exceptions expand_keywords hitless_words
59
- html_index_attrs html_remove_elements html_strip index_exact_words
60
- ignore_chars inplace_docinfo_gap inplace_enable inplace_hit_gap
61
- inplace_reloc_factor inplace_write_factor min_infix_len min_prefix_len
62
- min_stemming_len min_word_len mlock morphology ngram_chars ngram_len
63
- ondisk_dict overshort_step phrase_boundary phrase_boundary_step preopen
64
- stopwords stopwords_step wordforms )
65
-
66
- CustomOptions = %w( disable_range )
67
-
68
- attr_accessor :searchd_file_path, :allow_star, :database_yml_file,
69
- :app_root, :model_directories, :delayed_job_priority, :indexed_models
53
+ SourceOptions = Riddle::Configuration::SQLSource.settings.map { |setting|
54
+ setting.to_s
55
+ } - %w( type sql_query_pre sql_query sql_joined_field sql_file_field
56
+ sql_query_range sql_attr_uint sql_attr_bool sql_attr_bigint sql_query_info
57
+ sql_attr_timestamp sql_attr_str2ordinal sql_attr_float sql_attr_multi
58
+ sql_attr_string sql_attr_str2wordcount sql_column_buffers sql_field_string
59
+ sql_field_str2wordcount )
60
+ IndexOptions = Riddle::Configuration::Index.settings.map { |setting|
61
+ setting.to_s
62
+ } - %w( source prefix_fields infix_fields )
63
+ CustomOptions = %w( disable_range use_64_bit )
64
+
65
+ attr_accessor :searchd_file_path, :allow_star, :app_root,
66
+ :model_directories, :delayed_job_priority, :indexed_models, :use_64_bit,
67
+ :touched_reindex_file, :stop_timeout, :version
70
68
 
71
69
  attr_accessor :source_options, :index_options
72
- attr_accessor :version
73
70
 
74
- attr_reader :environment, :configuration, :controller
71
+ attr_reader :configuration, :controller
75
72
 
76
73
  @@environment = nil
77
74
 
@@ -107,9 +104,9 @@ module ThinkingSphinx
107
104
 
108
105
  self.address = "127.0.0.1"
109
106
  self.port = 9312
110
- self.database_yml_file = "#{self.app_root}/config/database.yml"
111
107
  self.searchd_file_path = "#{self.app_root}/db/sphinx/#{environment}"
112
108
  self.allow_star = false
109
+ self.stop_timeout = 5
113
110
  self.model_directories = ["#{app_root}/app/models/"] +
114
111
  Dir.glob("#{app_root}/vendor/plugins/*/app/models/")
115
112
  self.delayed_job_priority = 0
@@ -128,21 +125,15 @@ module ThinkingSphinx
128
125
  end
129
126
 
130
127
  def self.environment
131
- if @@environment.nil?
132
- ThinkingSphinx.mutex.synchronize do
133
- @@environment ||= if defined?(Merb)
134
- Merb.environment
135
- elsif defined?(RAILS_ENV)
136
- RAILS_ENV
137
- elsif defined?(Sinatra)
138
- Sinatra::Application.environment.to_s
139
- else
140
- ENV['RAILS_ENV'] || 'development'
141
- end
142
- end
128
+ @@environment ||= if defined?(Merb)
129
+ Merb.environment
130
+ elsif defined?(RAILS_ENV)
131
+ RAILS_ENV
132
+ elsif defined?(Sinatra)
133
+ Sinatra::Application.environment.to_s
134
+ else
135
+ ENV['RAILS_ENV'] || 'development'
143
136
  end
144
-
145
- @@environment
146
137
  end
147
138
 
148
139
  def self.reset_environment
@@ -276,7 +267,12 @@ module ThinkingSphinx
276
267
  end
277
268
  end
278
269
  end
279
-
270
+
271
+ def touch_reindex_file(output)
272
+ return FileUtils.touch(@touched_reindex_file) if @touched_reindex_file and output =~ /succesfully sent SIGHUP to searchd/
273
+ false
274
+ end
275
+
280
276
  private
281
277
 
282
278
  # Parse the config/sphinx.yml file - if it exists - then use the attribute
@@ -55,12 +55,13 @@ class ThinkingSphinx::Context
55
55
  model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
56
56
 
57
57
  next if model_name.nil?
58
+ camelized_model = model_name.camelize
58
59
  next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
59
- model.name == model_name.camelize
60
+ model.name == camelized_model
60
61
  }
61
62
 
62
63
  begin
63
- model_name.camelize.constantize
64
+ camelized_model.constantize
64
65
  rescue LoadError
65
66
  model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
66
67
  rescue NameError
@@ -61,27 +61,25 @@ DESC
61
61
 
62
62
  desc "Start the Sphinx daemon"
63
63
  task :start do
64
- configure
65
- rake "thinking_sphinx:start"
64
+ rake "thinking_sphinx:configure thinking_sphinx:start"
66
65
  end
67
66
 
68
67
  desc "Stop the Sphinx daemon"
69
68
  task :stop do
70
- configure
71
- rake "thinking_sphinx:stop"
69
+ rake "thinking_sphinx:configure thinking_sphinx:stop"
72
70
  end
73
71
 
74
72
  desc "Stop and then start the Sphinx daemon"
75
73
  task :restart do
76
- stop
77
- start
74
+ rake "thinking_sphinx:configure thinking_sphinx:stop \
75
+ thinking_sphinx:start"
78
76
  end
79
77
 
80
78
  desc "Stop, re-index and then start the Sphinx daemon"
81
79
  task :rebuild do
82
- stop
83
- index
84
- start
80
+ rake "thinking_sphinx:configure thinking_sphinx:stop \
81
+ thinking_sphinx:reindex \
82
+ thinking_sphinx:start"
85
83
  end
86
84
 
87
85
  desc "Add the shared folder for sphinx files"
@@ -6,7 +6,7 @@ module ThinkingSphinx
6
6
  # Most times, you will just want a specific model's results - to search and
7
7
  # search_for_ids methods will do the job in exactly the same manner when
8
8
  # called from a model.
9
- #
9
+ #
10
10
  class Search < Array
11
11
  CoreMethods = %w( == class class_eval extend frozen? id instance_eval
12
12
  instance_of? instance_values instance_variable_defined?
@@ -15,145 +15,145 @@ module ThinkingSphinx
15
15
  respond_to_missing? send should type )
16
16
  SafeMethods = %w( partition private_methods protected_methods
17
17
  public_methods send class )
18
-
18
+
19
19
  instance_methods.select { |method|
20
20
  method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
21
21
  }.each { |method|
22
22
  undef_method method
23
23
  }
24
-
24
+
25
25
  HashOptions = [:conditions, :with, :without, :with_all, :without_any]
26
26
  ArrayOptions = [:classes, :without_ids]
27
-
27
+
28
28
  attr_reader :args, :options
29
-
29
+
30
30
  # Deprecated. Use ThinkingSphinx.search
31
31
  def self.search(*args)
32
32
  warn 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
33
33
  ThinkingSphinx.search(*args)
34
34
  end
35
-
35
+
36
36
  # Deprecated. Use ThinkingSphinx.search_for_ids
37
37
  def self.search_for_ids(*args)
38
38
  warn 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
39
39
  ThinkingSphinx.search_for_ids(*args)
40
40
  end
41
-
41
+
42
42
  # Deprecated. Use ThinkingSphinx.search_for_ids
43
43
  def self.search_for_id(*args)
44
44
  warn 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
45
45
  ThinkingSphinx.search_for_id(*args)
46
46
  end
47
-
47
+
48
48
  # Deprecated. Use ThinkingSphinx.count
49
49
  def self.count(*args)
50
50
  warn 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
51
51
  ThinkingSphinx.count(*args)
52
52
  end
53
-
53
+
54
54
  # Deprecated. Use ThinkingSphinx.facets
55
55
  def self.facets(*args)
56
56
  warn 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
57
57
  ThinkingSphinx.facets(*args)
58
58
  end
59
-
59
+
60
60
  def self.bundle_searches(enum = nil)
61
61
  bundle = ThinkingSphinx::BundledSearch.new
62
-
62
+
63
63
  if enum.nil?
64
64
  yield bundle
65
65
  else
66
66
  enum.each { |item| yield bundle, item }
67
67
  end
68
-
68
+
69
69
  bundle.searches
70
70
  end
71
-
71
+
72
72
  def self.matching_fields(fields, bitmask)
73
73
  matches = []
74
74
  bitstring = bitmask.to_s(2).rjust(32, '0').reverse
75
-
75
+
76
76
  fields.each_with_index do |field, index|
77
77
  matches << field if bitstring[index, 1] == '1'
78
78
  end
79
79
  matches
80
80
  end
81
-
81
+
82
82
  def initialize(*args)
83
83
  ThinkingSphinx.context.define_indexes
84
-
84
+
85
85
  @array = []
86
86
  @options = args.extract_options!
87
87
  @args = args
88
-
88
+
89
89
  add_default_scope unless options[:ignore_default]
90
-
90
+
91
91
  populate if @options[:populate]
92
92
  end
93
-
93
+
94
94
  def to_a
95
95
  populate
96
96
  @array
97
97
  end
98
-
98
+
99
99
  def freeze
100
100
  populate
101
101
  @array.freeze
102
102
  self
103
103
  end
104
-
104
+
105
105
  # Indication of whether the request has been made to Sphinx for the search
106
106
  # query.
107
- #
107
+ #
108
108
  # @return [Boolean] true if the results have been requested.
109
- #
109
+ #
110
110
  def populated?
111
111
  !!@populated
112
112
  end
113
-
113
+
114
114
  # Indication of whether the request resulted in an error from Sphinx.
115
- #
115
+ #
116
116
  # @return [Boolean] true if Sphinx reports query error
117
- #
117
+ #
118
118
  def error?
119
119
  !!error
120
120
  end
121
-
121
+
122
122
  # The Sphinx-reported error, if any.
123
- #
123
+ #
124
124
  # @return [String, nil]
125
- #
125
+ #
126
126
  def error
127
127
  populate
128
128
  @results[:error]
129
129
  end
130
-
130
+
131
131
  # Indication of whether the request resulted in a warning from Sphinx.
132
- #
132
+ #
133
133
  # @return [Boolean] true if Sphinx reports query warning
134
- #
134
+ #
135
135
  def warning?
136
136
  !!warning
137
137
  end
138
-
138
+
139
139
  # The Sphinx-reported warning, if any.
140
- #
140
+ #
141
141
  # @return [String, nil]
142
- #
142
+ #
143
143
  def warning
144
144
  populate
145
145
  @results[:warning]
146
146
  end
147
-
147
+
148
148
  # The query result hash from Riddle.
149
- #
149
+ #
150
150
  # @return [Hash] Raw Sphinx results
151
- #
151
+ #
152
152
  def results
153
153
  populate
154
154
  @results
155
155
  end
156
-
156
+
157
157
  def method_missing(method, *args, &block)
158
158
  if is_scope?(method)
159
159
  add_scope(method, *args, &block)
@@ -166,62 +166,70 @@ module ThinkingSphinx
166
166
  elsif !SafeMethods.include?(method.to_s)
167
167
  populate
168
168
  end
169
-
169
+
170
170
  if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
171
171
  each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
172
172
  else
173
173
  @array.send(method, *args, &block)
174
174
  end
175
175
  end
176
-
176
+
177
177
  # Returns true if the Search object or the underlying Array object respond
178
178
  # to the requested method.
179
- #
179
+ #
180
180
  # @param [Symbol] method The method name
181
181
  # @return [Boolean] true if either Search or Array responds to the method.
182
- #
182
+ #
183
183
  def respond_to?(method, include_private = false)
184
184
  super || @array.respond_to?(method, include_private)
185
185
  end
186
-
186
+
187
187
  # The current page number of the result set. Defaults to 1 if no page was
188
188
  # explicitly requested.
189
- #
189
+ #
190
190
  # @return [Integer]
191
- #
191
+ #
192
192
  def current_page
193
193
  @options[:page].blank? ? 1 : @options[:page].to_i
194
194
  end
195
-
195
+
196
+ def first_page?
197
+ current_page == 1
198
+ end
199
+
196
200
  # Kaminari support
197
201
  def page(page_number)
198
202
  @options[:page] = page_number
199
203
  self
200
204
  end
201
-
205
+
202
206
  # The next page number of the result set. If there are no more pages
203
207
  # available, nil is returned.
204
- #
208
+ #
205
209
  # @return [Integer, nil]
206
- #
210
+ #
207
211
  def next_page
208
212
  current_page >= total_pages ? nil : current_page + 1
209
213
  end
210
-
214
+
215
+ def next_page?
216
+ !next_page.nil?
217
+ end
218
+
211
219
  # The previous page number of the result set. If this is the first page,
212
220
  # then nil is returned.
213
- #
221
+ #
214
222
  # @return [Integer, nil]
215
- #
223
+ #
216
224
  def previous_page
217
225
  current_page == 1 ? nil : current_page - 1
218
226
  end
219
-
227
+
220
228
  # The amount of records per set of paged results. Defaults to 20 unless a
221
229
  # specific page size is requested.
222
- #
230
+ #
223
231
  # @return [Integer]
224
- #
232
+ #
225
233
  def per_page
226
234
  @options[:limit] ||= @options[:per_page]
227
235
  @options[:limit] ||= 20
@@ -229,29 +237,29 @@ module ThinkingSphinx
229
237
  end
230
238
  # Kaminari support
231
239
  alias_method :limit_value, :per_page
232
-
240
+
233
241
  # Kaminari support
234
242
  def per(limit)
235
243
  @options[:limit] = limit
236
244
  self
237
245
  end
238
-
246
+
239
247
  # The total number of pages available if the results are paginated.
240
- #
248
+ #
241
249
  # @return [Integer]
242
- #
250
+ #
243
251
  def total_pages
244
252
  populate
245
253
  return 0 if @results[:total].nil?
246
-
254
+
247
255
  @total_pages ||= (@results[:total] / per_page.to_f).ceil
248
256
  end
249
257
  # Compatibility with kaminari and older versions of will_paginate
250
258
  alias_method :page_count, :total_pages
251
259
  alias_method :num_pages, :total_pages
252
-
260
+
253
261
  # Query time taken
254
- #
262
+ #
255
263
  # @return [Integer]
256
264
  #
257
265
  def query_time
@@ -262,34 +270,37 @@ module ThinkingSphinx
262
270
  end
263
271
 
264
272
  # The total number of search results available.
265
- #
273
+ #
266
274
  # @return [Integer]
267
- #
275
+ #
268
276
  def total_entries
269
277
  populate
270
278
  return 0 if @results[:total_found].nil?
271
-
279
+
272
280
  @total_entries ||= @results[:total_found]
273
281
  end
274
-
282
+
283
+ # Compatibility with kaminari
284
+ alias_method :total_count, :total_entries
285
+
275
286
  # The current page's offset, based on the number of records per page.
276
- # Or explicit :offset if given.
277
- #
287
+ # Or explicit :offset if given.
288
+ #
278
289
  # @return [Integer]
279
- #
290
+ #
280
291
  def offset
281
292
  @options[:offset] || ((current_page - 1) * per_page)
282
293
  end
283
-
294
+
284
295
  def indexes
285
296
  return options[:index] if options[:index]
286
297
  return '*' if classes.empty?
287
-
298
+
288
299
  classes.collect { |klass|
289
300
  klass.sphinx_index_names
290
301
  }.flatten.uniq.join(',')
291
302
  end
292
-
303
+
293
304
  def each_with_groupby_and_count(&block)
294
305
  populate
295
306
  results[:matches].each_with_index do |match, index|
@@ -299,42 +310,44 @@ module ThinkingSphinx
299
310
  end
300
311
  end
301
312
  alias_method :each_with_group_and_count, :each_with_groupby_and_count
302
-
313
+
303
314
  def each_with_weighting(&block)
304
315
  populate
305
316
  results[:matches].each_with_index do |match, index|
306
317
  yield self[index], match[:weight]
307
318
  end
308
319
  end
309
-
320
+
310
321
  def each_with_match(&block)
311
322
  populate
312
323
  results[:matches].each_with_index do |match, index|
313
324
  yield self[index], match
314
325
  end
315
326
  end
316
-
327
+
317
328
  def excerpt_for(string, model = nil)
318
329
  if model.nil? && one_class
319
330
  model ||= one_class
320
331
  end
321
-
332
+
322
333
  populate
334
+
335
+ index = options[:index] || "#{model.core_index_names.first}"
323
336
  client.excerpts(
324
337
  {
325
338
  :docs => [string.to_s],
326
339
  :words => results[:words].keys.join(' '),
327
- :index => options[:index] || "#{model.core_index_names.first}"
340
+ :index => index.split(',').first.strip
328
341
  }.merge(options[:excerpt_options] || {})
329
342
  ).first
330
343
  end
331
-
344
+
332
345
  def search(*args)
333
346
  args << args.extract_options!.merge(:ignore_default => true)
334
347
  merge_search ThinkingSphinx::Search.new(*args), self.args, options
335
348
  self
336
349
  end
337
-
350
+
338
351
  def search_for_ids(*args)
339
352
  args << args.extract_options!.merge(
340
353
  :ignore_default => true,
@@ -343,45 +356,45 @@ module ThinkingSphinx
343
356
  merge_search ThinkingSphinx::Search.new(*args), self.args, options
344
357
  self
345
358
  end
346
-
359
+
347
360
  def facets(*args)
348
361
  options = args.extract_options!
349
362
  merge_search self, args, options
350
363
  args << options
351
-
364
+
352
365
  ThinkingSphinx::FacetSearch.new(*args)
353
366
  end
354
-
367
+
355
368
  def client
356
369
  client = options[:client] || config.client
357
-
370
+
358
371
  prepare client
359
372
  end
360
-
373
+
361
374
  def append_to(client)
362
375
  prepare client
363
376
  client.append_query query, indexes, comment
364
377
  client.reset
365
378
  end
366
-
379
+
367
380
  def populate_from_queue(results)
368
381
  return if @populated
369
382
  @populated = true
370
383
  @results = results
371
-
384
+
372
385
  compose_results
373
386
  end
374
-
387
+
375
388
  private
376
-
389
+
377
390
  def config
378
391
  ThinkingSphinx::Configuration.instance
379
392
  end
380
-
393
+
381
394
  def populate
382
395
  return if @populated
383
396
  @populated = true
384
-
397
+
385
398
  retry_on_stale_index do
386
399
  begin
387
400
  log "Querying: '#{query}'"
@@ -390,9 +403,9 @@ module ThinkingSphinx
390
403
  }
391
404
  log "Found #{@results[:total_found]} results", :debug,
392
405
  "Sphinx (#{sprintf("%f", runtime)}s)"
393
-
406
+
394
407
  log "Sphinx Daemon returned warning: #{warning}", :error if warning?
395
-
408
+
396
409
  if error?
397
410
  log "Sphinx Daemon returned error: #{error}", :error
398
411
  raise SphinxError.new(error, @results) unless options[:ignore_errors]
@@ -401,7 +414,7 @@ module ThinkingSphinx
401
414
  raise ThinkingSphinx::ConnectionError,
402
415
  'Connection to Sphinx Daemon (searchd) failed.'
403
416
  end
404
-
417
+
405
418
  compose_results
406
419
  end
407
420
  end
@@ -418,13 +431,13 @@ module ThinkingSphinx
418
431
  add_matching_fields if client.rank_mode == :fieldmask
419
432
  end
420
433
  end
421
-
434
+
422
435
  def compose_ids_results
423
436
  replace @results[:matches].collect { |match|
424
437
  match[:attributes]['sphinx_internal_id']
425
438
  }
426
439
  end
427
-
440
+
428
441
  def compose_only_results
429
442
  replace @results[:matches].collect { |match|
430
443
  case only = options[:only]
@@ -440,30 +453,30 @@ module ThinkingSphinx
440
453
  end
441
454
  }
442
455
  end
443
-
456
+
444
457
  def add_excerpter
445
458
  each do |object|
446
459
  next if object.nil?
447
-
460
+
448
461
  object.excerpts = ThinkingSphinx::Excerpter.new self, object
449
462
  end
450
463
  end
451
-
464
+
452
465
  def add_sphinx_attributes
453
466
  each do |object|
454
467
  next if object.nil?
455
-
468
+
456
469
  match = match_hash object
457
470
  next if match.nil?
458
-
471
+
459
472
  object.sphinx_attributes = match[:attributes]
460
473
  end
461
474
  end
462
-
475
+
463
476
  def add_matching_fields
464
477
  each do |object|
465
478
  next if object.nil?
466
-
479
+
467
480
  match = match_hash object
468
481
  next if match.nil?
469
482
  object.matching_fields = ThinkingSphinx::Search.matching_fields(
@@ -471,21 +484,21 @@ module ThinkingSphinx
471
484
  )
472
485
  end
473
486
  end
474
-
487
+
475
488
  def match_hash(object)
476
489
  @results[:matches].detect { |match|
477
490
  class_crc = object.class.name
478
491
  class_crc = object.class.to_crc32 if Riddle.loaded_version.to_i < 2
479
-
492
+
480
493
  match[:attributes]['sphinx_internal_id'] == object.
481
494
  primary_key_for_sphinx &&
482
495
  match[:attributes][crc_attribute] == class_crc
483
496
  }
484
497
  end
485
-
498
+
486
499
  def self.log(message, method = :debug, identifier = 'Sphinx')
487
500
  return if ::ActiveRecord::Base.logger.nil?
488
-
501
+
489
502
  info = ''
490
503
  if ::ActiveRecord::Base.colorize_logging
491
504
  identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
@@ -494,20 +507,20 @@ module ThinkingSphinx
494
507
  else
495
508
  info = "#{identifier} #{message}"
496
509
  end
497
-
510
+
498
511
  ::ActiveRecord::Base.logger.send method, info
499
512
  end
500
-
513
+
501
514
  def log(*args)
502
515
  self.class.log(*args)
503
516
  end
504
-
517
+
505
518
  def prepare(client)
506
519
  index_options = {}
507
520
  if one_class && one_class.sphinx_indexes && one_class.sphinx_indexes.first
508
521
  index_options = one_class.sphinx_indexes.first.local_options
509
522
  end
510
-
523
+
511
524
  [
512
525
  :max_matches, :group_by, :group_function, :group_clause,
513
526
  :group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
@@ -519,7 +532,7 @@ module ThinkingSphinx
519
532
 
520
533
  # treated non-standard as :select is already used for AR queries
521
534
  client.select = options[:sphinx_select] || '*'
522
-
535
+
523
536
  client.limit = per_page
524
537
  client.offset = offset
525
538
  client.match_mode = match_mode
@@ -530,62 +543,62 @@ module ThinkingSphinx
530
543
  client.group_function = group_function if group_function
531
544
  client.index_weights = index_weights
532
545
  client.anchor = anchor
533
-
546
+
534
547
  client
535
548
  end
536
-
549
+
537
550
  def retry_on_stale_index(&block)
538
551
  stale_ids = []
539
552
  retries = stale_retries
540
-
553
+
541
554
  begin
542
555
  options[:raise_on_stale] = retries > 0
543
556
  block.call
544
-
557
+
545
558
  # If ThinkingSphinx::Search#instances_from_matches found records in
546
559
  # Sphinx but not in the DB and the :raise_on_stale option is set, this
547
560
  # exception is raised. We retry a limited number of times, excluding the
548
561
  # stale ids from the search.
549
562
  rescue StaleIdsException => err
550
563
  retries -= 1
551
-
564
+
552
565
  # For logging
553
566
  stale_ids |= err.ids
554
567
  # ID exclusion
555
568
  options[:without_ids] = Array(options[:without_ids]) | err.ids
556
-
569
+
557
570
  log 'Sphinx Stale Ids (%s %s left): %s' % [
558
571
  retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
559
572
  ]
560
573
  retry
561
574
  end
562
575
  end
563
-
576
+
564
577
  def classes
565
578
  @classes ||= options[:classes] || []
566
579
  end
567
-
580
+
568
581
  def one_class
569
582
  @one_class ||= classes.length != 1 ? nil : classes.first
570
583
  end
571
-
584
+
572
585
  def query
573
586
  @query ||= begin
574
587
  q = @args.join(' ') << conditions_as_query
575
588
  (options[:star] ? star_query(q) : q).strip
576
589
  end
577
590
  end
578
-
591
+
579
592
  def conditions_as_query
580
593
  return '' if @options[:conditions].blank?
581
-
594
+
582
595
  ' ' + @options[:conditions].keys.collect { |key|
583
596
  "@#{key} #{options[:conditions][key]}"
584
597
  }.join(' ')
585
598
  end
586
-
599
+
587
600
  def star_query(query)
588
- token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
601
+ token = options[:star].is_a?(Regexp) ? options[:star] : default_star_token
589
602
 
590
603
  query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
591
604
  pre, proper, post = $`, $&, $'
@@ -602,15 +615,25 @@ module ThinkingSphinx
602
615
  end
603
616
  end
604
617
  end
605
-
618
+
619
+ if Regexp.instance_methods.include?(:encoding)
620
+ DefaultStarToken = Regexp.new('\p{Word}+')
621
+ else
622
+ DefaultStarToken = Regexp.new('\w+', nil, 'u')
623
+ end
624
+
625
+ def default_star_token
626
+ DefaultStarToken
627
+ end
628
+
606
629
  def comment
607
630
  options[:comment] || ''
608
631
  end
609
-
632
+
610
633
  def match_mode
611
634
  options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
612
635
  end
613
-
636
+
614
637
  def sort_mode
615
638
  @sort_mode ||= case options[:sort_mode]
616
639
  when :asc
@@ -630,7 +653,7 @@ module ThinkingSphinx
630
653
  options[:sort_mode]
631
654
  end
632
655
  end
633
-
656
+
634
657
  def sort_by
635
658
  case @sort_by = (options[:sort_by] || options[:order])
636
659
  when String
@@ -642,28 +665,28 @@ module ThinkingSphinx
642
665
  ''
643
666
  end
644
667
  end
645
-
668
+
646
669
  def field_names
647
670
  return [] unless one_class
648
-
671
+
649
672
  one_class.sphinx_indexes.collect { |index|
650
673
  index.fields.collect { |field| field.unique_name }
651
674
  }.flatten
652
675
  end
653
-
676
+
654
677
  def sorted_fields_to_attributes(order_string)
655
678
  field_names.each { |field|
656
679
  order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
657
680
  match.gsub field.to_s, field.to_s.concat("_sort")
658
681
  }
659
682
  }
660
-
683
+
661
684
  order_string
662
685
  end
663
-
686
+
664
687
  # Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
665
688
  # { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
666
- #
689
+ #
667
690
  def index_weights
668
691
  weights = options[:index_weights] || {}
669
692
  weights.keys.inject({}) do |hash, key|
@@ -674,37 +697,37 @@ module ThinkingSphinx
674
697
  else
675
698
  hash[key] = weights[key]
676
699
  end
677
-
700
+
678
701
  hash
679
702
  end
680
703
  end
681
-
704
+
682
705
  def group_by
683
706
  options[:group] ? options[:group].to_s : nil
684
707
  end
685
-
708
+
686
709
  def group_function
687
710
  options[:group] ? :attr : nil
688
711
  end
689
-
712
+
690
713
  def internal_filters
691
714
  filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
692
-
715
+
693
716
  class_crcs = classes.collect { |klass|
694
717
  klass.to_crc32s
695
718
  }.flatten
696
-
719
+
697
720
  unless class_crcs.empty?
698
721
  filters << Riddle::Client::Filter.new('class_crc', class_crcs)
699
722
  end
700
-
723
+
701
724
  filters << Riddle::Client::Filter.new(
702
725
  'sphinx_internal_id', filter_value(options[:without_ids]), true
703
- ) if options[:without_ids]
704
-
726
+ ) unless options[:without_ids].nil? || options[:without_ids].empty?
727
+
705
728
  filters
706
729
  end
707
-
730
+
708
731
  def filters
709
732
  internal_filters +
710
733
  (options[:with] || {}).collect { |attrib, value|
@@ -724,7 +747,7 @@ module ThinkingSphinx
724
747
  }
725
748
  }.flatten
726
749
  end
727
-
750
+
728
751
  # When passed a Time instance, returns the integer timestamp.
729
752
  def filter_value(value)
730
753
  case value
@@ -740,10 +763,10 @@ module ThinkingSphinx
740
763
  Array(value)
741
764
  end
742
765
  end
743
-
766
+
744
767
  def anchor
745
768
  return {} unless options[:geo] || (options[:lat] && options[:lng])
746
-
769
+
747
770
  {
748
771
  :latitude => options[:geo] ? options[:geo].first : options[:lat],
749
772
  :longitude => options[:geo] ? options[:geo].last : options[:lng],
@@ -751,43 +774,43 @@ module ThinkingSphinx
751
774
  :longitude_attribute => longitude_attr.to_s
752
775
  }
753
776
  end
754
-
777
+
755
778
  def latitude_attr
756
779
  options[:latitude_attr] ||
757
780
  index_option(:latitude_attr) ||
758
781
  attribute(:lat, :latitude)
759
782
  end
760
-
783
+
761
784
  def longitude_attr
762
785
  options[:longitude_attr] ||
763
786
  index_option(:longitude_attr) ||
764
787
  attribute(:lon, :lng, :longitude)
765
788
  end
766
-
789
+
767
790
  def index_option(key)
768
791
  return nil unless one_class
769
-
792
+
770
793
  one_class.sphinx_indexes.collect { |index|
771
794
  index.local_options[key]
772
795
  }.compact.first
773
796
  end
774
-
797
+
775
798
  def attribute(*keys)
776
799
  return nil unless one_class
777
-
800
+
778
801
  keys.detect { |key|
779
802
  attributes.include?(key)
780
803
  }
781
804
  end
782
-
805
+
783
806
  def attributes
784
807
  return [] unless one_class
785
-
808
+
786
809
  attributes = one_class.sphinx_indexes.collect { |index|
787
810
  index.attributes.collect { |attrib| attrib.unique_name }
788
811
  }.flatten
789
812
  end
790
-
813
+
791
814
  def stale_retries
792
815
  case options[:retry_stale]
793
816
  when TrueClass
@@ -798,10 +821,10 @@ module ThinkingSphinx
798
821
  options[:retry_stale].to_i
799
822
  end
800
823
  end
801
-
824
+
802
825
  def include_for_class(klass)
803
826
  includes = options[:include] || klass.sphinx_index_options[:include]
804
-
827
+
805
828
  case includes
806
829
  when NilClass
807
830
  nil
@@ -829,7 +852,7 @@ module ThinkingSphinx
829
852
  end
830
853
  scoped_array.empty? ? nil : scoped_array
831
854
  end
832
-
855
+
833
856
  def include_from_hash(hash, klass)
834
857
  scoped_hash = {}
835
858
  hash.keys.each do |key|
@@ -837,7 +860,7 @@ module ThinkingSphinx
837
860
  end
838
861
  scoped_hash.empty? ? nil : scoped_hash
839
862
  end
840
-
863
+
841
864
  def instances_from_class(klass, matches)
842
865
  index_options = klass.sphinx_index_options
843
866
 
@@ -852,7 +875,7 @@ module ThinkingSphinx
852
875
  ) : []
853
876
 
854
877
  # Raise an exception if we find records in Sphinx but not in the DB, so
855
- # the search method can retry without them. See
878
+ # the search method can retry without them. See
856
879
  # ThinkingSphinx::Search.retry_search_on_stale_index.
857
880
  if options[:raise_on_stale] && instances.length < ids.length
858
881
  stale_ids = ids - instances.map { |i| i.id }
@@ -869,13 +892,13 @@ module ThinkingSphinx
869
892
  end
870
893
  }
871
894
  end
872
-
895
+
873
896
  # Group results by class and call #find(:all) once for each group to reduce
874
897
  # the number of #find's in multi-model searches.
875
- #
898
+ #
876
899
  def instances_from_matches
877
900
  return single_class_results if one_class
878
-
901
+
879
902
  groups = results[:matches].group_by { |match|
880
903
  match[:attributes][crc_attribute]
881
904
  }
@@ -884,7 +907,7 @@ module ThinkingSphinx
884
907
  instances_from_class(class_from_crc(crc), group)
885
908
  )
886
909
  end
887
-
910
+
888
911
  results[:matches].collect do |match|
889
912
  groups.detect { |crc, group|
890
913
  crc == match[:attributes][crc_attribute]
@@ -893,11 +916,11 @@ module ThinkingSphinx
893
916
  }
894
917
  end
895
918
  end
896
-
919
+
897
920
  def single_class_results
898
921
  instances_from_class one_class, results[:matches]
899
922
  end
900
-
923
+
901
924
  def class_from_crc(crc)
902
925
  if Riddle.loaded_version.to_i < 2
903
926
  config.models_by_crc[crc].constantize
@@ -905,7 +928,7 @@ module ThinkingSphinx
905
928
  crc.constantize
906
929
  end
907
930
  end
908
-
931
+
909
932
  def each_with_attribute(attribute, &block)
910
933
  populate
911
934
  results[:matches].each_with_index do |match, index|
@@ -913,25 +936,25 @@ module ThinkingSphinx
913
936
  (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
914
937
  end
915
938
  end
916
-
939
+
917
940
  def is_scope?(method)
918
941
  one_class && one_class.sphinx_scopes.include?(method)
919
942
  end
920
-
943
+
921
944
  # Adds the default_sphinx_scope if set.
922
945
  def add_default_scope
923
946
  return unless one_class && one_class.has_default_sphinx_scope?
924
947
  add_scope(one_class.get_default_sphinx_scope.to_sym)
925
948
  end
926
-
949
+
927
950
  def add_scope(method, *args, &block)
928
951
  method = "#{method}_without_default".to_sym
929
952
  merge_search one_class.send(method, *args, &block), self.args, options
930
953
  end
931
-
954
+
932
955
  def merge_search(search, args, options)
933
956
  search.args.each { |arg| args << arg }
934
-
957
+
935
958
  search.options.keys.each do |key|
936
959
  if HashOptions.include?(key)
937
960
  options[key] ||= {}
@@ -945,18 +968,18 @@ module ThinkingSphinx
945
968
  end
946
969
  end
947
970
  end
948
-
971
+
949
972
  def scoped_count
950
973
  return self.total_entries if(@options[:ids_only] || @options[:only])
951
-
974
+
952
975
  @options[:ids_only] = true
953
976
  results_count = self.total_entries
954
977
  @options[:ids_only] = false
955
978
  @populated = false
956
-
979
+
957
980
  results_count
958
981
  end
959
-
982
+
960
983
  def crc_attribute
961
984
  Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
962
985
  end