thinking-sphinx 2.0.0.rc2 → 2.0.0

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 (40) hide show
  1. data/README.textile +6 -0
  2. data/VERSION +1 -1
  3. data/features/excerpts.feature +8 -0
  4. data/features/field_sorting.feature +18 -0
  5. data/features/searching_across_models.feature +1 -1
  6. data/features/searching_by_model.feature +0 -7
  7. data/features/sphinx_scopes.feature +18 -0
  8. data/features/step_definitions/common_steps.rb +4 -0
  9. data/features/step_definitions/search_steps.rb +5 -0
  10. data/features/support/env.rb +4 -5
  11. data/features/thinking_sphinx/db/fixtures/people.rb +1 -1
  12. data/features/thinking_sphinx/models/alpha.rb +1 -0
  13. data/features/thinking_sphinx/models/andrew.rb +17 -0
  14. data/features/thinking_sphinx/models/person.rb +2 -1
  15. data/lib/thinking_sphinx.rb +3 -0
  16. data/lib/thinking_sphinx/active_record.rb +1 -1
  17. data/lib/thinking_sphinx/active_record/scopes.rb +7 -0
  18. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +38 -8
  19. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +6 -2
  20. data/lib/thinking_sphinx/association.rb +19 -14
  21. data/lib/thinking_sphinx/attribute.rb +5 -0
  22. data/lib/thinking_sphinx/auto_version.rb +2 -0
  23. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  24. data/lib/thinking_sphinx/configuration.rb +14 -10
  25. data/lib/thinking_sphinx/context.rb +4 -2
  26. data/lib/thinking_sphinx/property.rb +1 -0
  27. data/lib/thinking_sphinx/railtie.rb +2 -2
  28. data/lib/thinking_sphinx/search.rb +74 -48
  29. data/lib/thinking_sphinx/source/sql.rb +1 -1
  30. data/lib/thinking_sphinx/tasks.rb +7 -0
  31. data/spec/thinking_sphinx/active_record/scopes_spec.rb +2 -3
  32. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +134 -0
  33. data/spec/thinking_sphinx/association_spec.rb +1 -24
  34. data/spec/thinking_sphinx/auto_version_spec.rb +8 -0
  35. data/spec/thinking_sphinx/configuration_spec.rb +11 -4
  36. data/spec/thinking_sphinx/context_spec.rb +3 -2
  37. data/spec/thinking_sphinx/search_spec.rb +67 -25
  38. data/tasks/distribution.rb +0 -6
  39. data/tasks/testing.rb +25 -15
  40. metadata +279 -67
@@ -53,15 +53,16 @@ module ThinkingSphinx
53
53
  mysql_ssl_ca sql_range_step sql_query_pre sql_query_post
54
54
  sql_query_killlist sql_ranged_throttle sql_query_post_index unpack_zlib
55
55
  unpack_mysqlcompress unpack_mysqlcompress_maxsize )
56
-
57
- IndexOptions = %w( charset_table charset_type charset_dictpath docinfo
58
- enable_star exceptions html_index_attrs html_remove_elements html_strip
59
- index_exact_words ignore_chars inplace_docinfo_gap inplace_enable
60
- inplace_hit_gap inplace_reloc_factor inplace_write_factor min_infix_len
61
- min_prefix_len min_stemming_len min_word_len mlock morphology ngram_chars
62
- ngram_len ondisk_dict overshort_step phrase_boundary phrase_boundary_step
63
- preopen stopwords stopwords_step wordforms )
64
-
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
+
65
66
  CustomOptions = %w( disable_range )
66
67
 
67
68
  attr_accessor :searchd_file_path, :allow_star, :database_yml_file,
@@ -232,10 +233,13 @@ module ThinkingSphinx
232
233
  def indexer_binary_name=(name)
233
234
  @controller.indexer_binary_name = name
234
235
  end
235
-
236
+
237
+ attr_accessor :timeout
238
+
236
239
  def client
237
240
  client = Riddle::Client.new address, port
238
241
  client.max_matches = configuration.searchd.max_matches || 1000
242
+ client.timeout = timeout || 0
239
243
  client
240
244
  end
241
245
 
@@ -65,8 +65,10 @@ class ThinkingSphinx::Context
65
65
  model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
66
66
  rescue NameError
67
67
  next
68
- rescue StandardError
69
- STDERR.puts "Warning: Error loading #{file}"
68
+ rescue StandardError => err
69
+ STDERR.puts "Warning: Error loading #{file}:"
70
+ STDERR.puts err.message
71
+ STDERR.puts err.backtrace.join("\n"), ''
70
72
  end
71
73
  end
72
74
  end
@@ -13,6 +13,7 @@ module ThinkingSphinx
13
13
  @alias = options[:as]
14
14
  @faceted = options[:facet]
15
15
  @admin = options[:admin]
16
+ @sortable = options[:sortable] || false
16
17
 
17
18
  @alias = @alias.to_sym unless @alias.blank?
18
19
 
@@ -5,8 +5,8 @@ module ThinkingSphinx
5
5
  class Railtie < Rails::Railtie
6
6
 
7
7
  initializer "thinking_sphinx.active_record" do
8
- if defined?(ActiveRecord)
9
- ::ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
8
+ ActiveSupport.on_load :active_record do
9
+ include ThinkingSphinx::ActiveRecord
10
10
  end
11
11
  end
12
12
 
@@ -57,20 +57,16 @@ module ThinkingSphinx
57
57
  ThinkingSphinx.facets *args
58
58
  end
59
59
 
60
- def self.bundle_searches(enum)
61
- client = ThinkingSphinx::Configuration.instance.client
60
+ def self.bundle_searches(enum = nil)
61
+ bundle = ThinkingSphinx::BundledSearch.new
62
62
 
63
- searches = enum.collect { |item|
64
- search = yield ThinkingSphinx, item
65
- search.append_to client
66
- search
67
- }
68
-
69
- client.run.each_with_index.collect { |results, index|
70
- searches[index].populate_from_queue results
71
- }
63
+ if enum.nil?
64
+ yield bundle
65
+ else
66
+ enum.each { |item| yield bundle, item }
67
+ end
72
68
 
73
- searches
69
+ bundle.searches
74
70
  end
75
71
 
76
72
  def self.matching_fields(fields, bitmask)
@@ -90,6 +86,8 @@ module ThinkingSphinx
90
86
  @options = args.extract_options!
91
87
  @args = args
92
88
 
89
+ add_default_scope unless options[:ignore_default]
90
+
93
91
  populate if @options[:populate]
94
92
  end
95
93
 
@@ -127,7 +125,7 @@ module ThinkingSphinx
127
125
  add_scope(method, *args, &block)
128
126
  return self
129
127
  elsif method == :search_count
130
- merge_search one_class.search(*args)
128
+ merge_search one_class.search(*args), self.args, options
131
129
  return scoped_count
132
130
  elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
133
131
  super
@@ -274,18 +272,43 @@ module ThinkingSphinx
274
272
 
275
273
  populate
276
274
  client.excerpts(
277
- :docs => [string],
278
- :words => results[:words].keys.join(' '),
279
- :index => options[:index] || "#{model.source_of_sphinx_index.sphinx_name}_core"
275
+ {
276
+ :docs => [string.to_s],
277
+ :words => results[:words].keys.join(' '),
278
+ :index => options[:index] || "#{model.source_of_sphinx_index.sphinx_name}_core"
279
+ }.merge(options[:excerpt_options] || {})
280
280
  ).first
281
281
  end
282
282
 
283
283
  def search(*args)
284
- add_default_scope
285
- merge_search ThinkingSphinx::Search.new(*args)
284
+ args << args.extract_options!.merge(:ignore_default => true)
285
+ merge_search ThinkingSphinx::Search.new(*args), self.args, options
286
286
  self
287
287
  end
288
288
 
289
+ def search_for_ids(*args)
290
+ args << args.extract_options!.merge(
291
+ :ignore_default => true,
292
+ :ids_only => true
293
+ )
294
+ merge_search ThinkingSphinx::Search.new(*args), self.args, options
295
+ self
296
+ end
297
+
298
+ def facets(*args)
299
+ options = args.extract_options!
300
+ merge_search self, args, options
301
+ args << options
302
+
303
+ ThinkingSphinx::FacetSearch.new *args
304
+ end
305
+
306
+ def client
307
+ client = options[:client] || config.client
308
+
309
+ prepare client
310
+ end
311
+
289
312
  def append_to(client)
290
313
  prepare client
291
314
  client.append_query query, indexes, comment
@@ -386,9 +409,16 @@ module ThinkingSphinx
386
409
 
387
410
  def self.log(message, method = :debug, identifier = 'Sphinx')
388
411
  return if ::ActiveRecord::Base.logger.nil?
389
- identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
390
- info = " \e[#{identifier_color}m#{identifier}\e[0m "
391
- info << "\e[#{message_color}m#{message}\e[0m"
412
+
413
+ info = ''
414
+ if ::ActiveRecord::LogSubscriber.colorize_logging
415
+ identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
416
+ info << " \e[#{identifier_color}m#{identifier}\e[0m "
417
+ info << "\e[#{message_color}m#{message}\e[0m"
418
+ else
419
+ info = "#{identifier} #{message}"
420
+ end
421
+
392
422
  ::ActiveRecord::Base.logger.send method, info
393
423
  end
394
424
 
@@ -396,12 +426,6 @@ module ThinkingSphinx
396
426
  self.class.log(*args)
397
427
  end
398
428
 
399
- def client
400
- client = config.client
401
-
402
- prepare client
403
- end
404
-
405
429
  def prepare(client)
406
430
  index_options = one_class ?
407
431
  one_class.sphinx_indexes.first.local_options : {}
@@ -493,7 +517,8 @@ module ThinkingSphinx
493
517
  query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
494
518
  pre, proper, post = $`, $&, $'
495
519
  # E.g. "@foo", "/2", "~3", but not as part of a token
496
- is_operator = pre.match(%r{(\W|^)[@~/]\Z})
520
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z}) ||
521
+ pre.match(%r{(\W|^)@\([^\)]*$})
497
522
  # E.g. "foo bar", with quotes
498
523
  is_quote = proper.starts_with?('"') && proper.ends_with?('"')
499
524
  has_star = pre.ends_with?("*") || post.starts_with?("*")
@@ -607,24 +632,8 @@ module ThinkingSphinx
607
632
  filters
608
633
  end
609
634
 
610
- def condition_filters
611
- (options[:conditions] || {}).collect { |attrib, value|
612
- if attributes.include?(attrib.to_sym)
613
- puts <<-MSG
614
- Deprecation Warning: filters on attributes should be done using the :with
615
- option, not :conditions. For example:
616
- :with => {:#{attrib} => #{value.inspect}}
617
- MSG
618
- Riddle::Client::Filter.new attrib.to_s, filter_value(value)
619
- else
620
- nil
621
- end
622
- }.compact
623
- end
624
-
625
635
  def filters
626
636
  internal_filters +
627
- condition_filters +
628
637
  (options[:with] || {}).collect { |attrib, value|
629
638
  Riddle::Client::Filter.new attrib.to_s, filter_value(value)
630
639
  } +
@@ -712,6 +721,21 @@ MSG
712
721
  end
713
722
  end
714
723
 
724
+ def include_for_class(klass)
725
+ includes = options[:include] || klass.sphinx_index_options[:include]
726
+
727
+ case includes
728
+ when NilClass
729
+ nil
730
+ when Array
731
+ includes.select { |inc| klass.reflections[inc] }
732
+ when Symbol
733
+ klass.reflections[includes].nil? ? nil : includes
734
+ else
735
+ includes
736
+ end
737
+ end
738
+
715
739
  def instances_from_class(klass, matches)
716
740
  index_options = klass.sphinx_index_options
717
741
 
@@ -720,7 +744,7 @@ MSG
720
744
  :all,
721
745
  :joins => options[:joins],
722
746
  :conditions => {klass.primary_key_for_sphinx.to_sym => ids},
723
- :include => (options[:include] || index_options[:include]),
747
+ :include => include_for_class(klass),
724
748
  :select => (options[:select] || index_options[:select]),
725
749
  :order => (options[:sql_order] || index_options[:sql_order])
726
750
  ) : []
@@ -790,14 +814,16 @@ MSG
790
814
 
791
815
  # Adds the default_sphinx_scope if set.
792
816
  def add_default_scope
793
- add_scope(one_class.get_default_sphinx_scope) if one_class && one_class.has_default_sphinx_scope?
817
+ return unless one_class && one_class.has_default_sphinx_scope?
818
+ add_scope(one_class.get_default_sphinx_scope.to_sym)
794
819
  end
795
820
 
796
821
  def add_scope(method, *args, &block)
797
- merge_search one_class.send(method, *args, &block)
822
+ method = "#{method}_without_default".to_sym
823
+ merge_search one_class.send(method, *args, &block), self.args, options
798
824
  end
799
825
 
800
- def merge_search(search)
826
+ def merge_search(search, args, options)
801
827
  search.args.each { |arg| args << arg }
802
828
 
803
829
  search.options.keys.each do |key|
@@ -20,7 +20,7 @@ module ThinkingSphinx
20
20
  )
21
21
 
22
22
  all_associations.each do |assoc|
23
- relation = relation.joins(assoc.join.with_join_class(Arel::OuterJoin))
23
+ relation = relation.joins(assoc.arel_join)
24
24
  end
25
25
 
26
26
  relation = relation.where(sql_where_clause(options))
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'timeout'
2
3
 
3
4
  namespace :thinking_sphinx do
4
5
  task :app_env do
@@ -47,6 +48,12 @@ namespace :thinking_sphinx do
47
48
  config = ThinkingSphinx::Configuration.instance
48
49
  pid = sphinx_pid
49
50
  config.controller.stop
51
+
52
+ # Ensure searchd is stopped, but don't try too hard
53
+ Timeout.timeout(5) do
54
+ sleep(1) until config.controller.stop
55
+ end
56
+
50
57
  puts "Stopped search daemon (pid #{pid})."
51
58
  end
52
59
  end
@@ -87,10 +87,9 @@ describe ThinkingSphinx::ActiveRecord::Scopes do
87
87
  search.by_foo('foo').search.options[:conditions].should == {:foo => 'foo', :name => 'foo'}
88
88
  end
89
89
 
90
- # FIXME: Probably the other way around is more logical? How to do this?
91
- it "should apply the default scope options after other scope options to the underlying search object" do
90
+ it "should apply the default scope options before other scope options to the underlying search object" do
92
91
  search = ThinkingSphinx::Search.new(:classes => [Alpha])
93
- search.by_name('bar').search.options[:conditions].should == {:name => 'foo'}
92
+ search.by_name('bar').search.options[:conditions].should == {:name => 'bar'}
94
93
  end
95
94
  end
96
95
 
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe ThinkingSphinx::AbstractAdapter do
4
+ describe '.detect' do
5
+ let(:model) { stub('model') }
6
+
7
+ it "returns a MysqlAdapter object for :mysql" do
8
+ ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => :mysql)
9
+
10
+ adapter = ThinkingSphinx::AbstractAdapter.detect(model)
11
+ adapter.should be_a(ThinkingSphinx::MysqlAdapter)
12
+ end
13
+
14
+ it "returns a PostgreSQLAdapter object for :postgresql" do
15
+ ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => :postgresql)
16
+
17
+ adapter = ThinkingSphinx::AbstractAdapter.detect(model)
18
+ adapter.should be_a(ThinkingSphinx::PostgreSQLAdapter)
19
+ end
20
+
21
+ it "raises an exception for other responses" do
22
+ ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => :sqlite)
23
+
24
+ lambda {
25
+ ThinkingSphinx::AbstractAdapter.detect(model)
26
+ }.should raise_error
27
+ end
28
+ end
29
+
30
+ describe '.adapter_for_model' do
31
+ let(:model) { stub('model') }
32
+
33
+ after :each do
34
+ ThinkingSphinx.database_adapter = nil
35
+ end
36
+
37
+ it "translates strings to symbols" do
38
+ ThinkingSphinx.database_adapter = 'foo'
39
+
40
+ ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :foo
41
+ end
42
+
43
+ it "passes through symbols unchanged" do
44
+ ThinkingSphinx.database_adapter = :bar
45
+
46
+ ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :bar
47
+ end
48
+
49
+ it "returns standard_adapter_for_model if database_adapter is not set" do
50
+ ThinkingSphinx.database_adapter = nil
51
+ ThinkingSphinx::AbstractAdapter.stub!(:standard_adapter_for_model => :baz)
52
+
53
+ ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :baz
54
+ end
55
+
56
+ it "calls the lambda and returns it if one is provided" do
57
+ ThinkingSphinx.database_adapter = lambda { |model| :foo }
58
+
59
+ ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :foo
60
+ end
61
+ end
62
+
63
+ describe '.standard_adapter_for_model' do
64
+ let(:klass) { stub('connection class') }
65
+ let(:connection) { stub('connection', :class => klass) }
66
+ let(:model) { stub('model', :connection => connection) }
67
+
68
+ it "translates a normal MySQL adapter" do
69
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::MysqlAdapter')
70
+
71
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
72
+ should == :mysql
73
+ end
74
+
75
+ it "translates a MySQL plus adapter" do
76
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::MysqlplusAdapter')
77
+
78
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
79
+ should == :mysql
80
+ end
81
+
82
+ it "translates a MySQL2 adapter" do
83
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::Mysql2Adapter')
84
+
85
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
86
+ should == :mysql
87
+ end
88
+
89
+ it "translates a NullDB adapter to MySQL" do
90
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::NullDBAdapter')
91
+
92
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
93
+ should == :mysql
94
+ end
95
+
96
+ it "translates a normal PostgreSQL adapter" do
97
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter')
98
+
99
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
100
+ should == :postgresql
101
+ end
102
+
103
+ it "translates a JDBC MySQL adapter to MySQL" do
104
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter')
105
+ connection.stub(:config => {:adapter => 'jdbcmysql'})
106
+
107
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
108
+ should == :mysql
109
+ end
110
+
111
+ it "translates a JDBC PostgreSQL adapter to PostgreSQL" do
112
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter')
113
+ connection.stub(:config => {:adapter => 'jdbcpostgresql'})
114
+
115
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
116
+ should == :postgresql
117
+ end
118
+
119
+ it "returns other JDBC adapters without translation" do
120
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter')
121
+ connection.stub(:config => {:adapter => 'jdbcmssql'})
122
+
123
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
124
+ should == 'jdbcmssql'
125
+ end
126
+
127
+ it "returns other unknown adapters without translation" do
128
+ klass.stub(:name => 'ActiveRecord::ConnectionAdapters::FooAdapter')
129
+
130
+ ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
131
+ should == 'ActiveRecord::ConnectionAdapters::FooAdapter'
132
+ end
133
+ end
134
+ end