thinking-sphinx 2.0.0.rc2 → 2.0.0

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