thinking-sphinx 1.4.6 → 1.4.7

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