thinking-sphinx 3.0.3 → 3.0.4

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 (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -1
  3. data/HISTORY +25 -0
  4. data/lib/thinking_sphinx.rb +1 -0
  5. data/lib/thinking_sphinx/active_record/association_proxy.rb +13 -55
  6. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +47 -0
  7. data/lib/thinking_sphinx/active_record/base.rb +16 -15
  8. data/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +1 -12
  9. data/lib/thinking_sphinx/active_record/database_adapters.rb +41 -42
  10. data/lib/thinking_sphinx/active_record/polymorpher.rb +7 -2
  11. data/lib/thinking_sphinx/active_record/property_query.rb +23 -19
  12. data/lib/thinking_sphinx/active_record/sql_builder.rb +108 -129
  13. data/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb +28 -0
  14. data/lib/thinking_sphinx/active_record/sql_builder/query.rb +43 -0
  15. data/lib/thinking_sphinx/active_record/sql_builder/statement.rb +110 -0
  16. data/lib/thinking_sphinx/active_record/sql_source.rb +143 -138
  17. data/lib/thinking_sphinx/capistrano.rb +11 -8
  18. data/lib/thinking_sphinx/configuration.rb +57 -35
  19. data/lib/thinking_sphinx/connection.rb +15 -6
  20. data/lib/thinking_sphinx/core.rb +1 -0
  21. data/lib/thinking_sphinx/core/index.rb +18 -10
  22. data/lib/thinking_sphinx/core/settings.rb +9 -0
  23. data/lib/thinking_sphinx/deletion.rb +48 -0
  24. data/lib/thinking_sphinx/errors.rb +7 -0
  25. data/lib/thinking_sphinx/excerpter.rb +1 -0
  26. data/lib/thinking_sphinx/facet_search.rb +42 -19
  27. data/lib/thinking_sphinx/masks/scopes_mask.rb +7 -0
  28. data/lib/thinking_sphinx/middlewares.rb +27 -33
  29. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +18 -18
  30. data/lib/thinking_sphinx/middlewares/geographer.rb +49 -16
  31. data/lib/thinking_sphinx/middlewares/sphinxql.rb +128 -58
  32. data/lib/thinking_sphinx/panes/excerpts_pane.rb +7 -3
  33. data/lib/thinking_sphinx/rake_interface.rb +10 -0
  34. data/lib/thinking_sphinx/real_time.rb +7 -1
  35. data/lib/thinking_sphinx/real_time/attribute.rb +4 -0
  36. data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +14 -10
  37. data/lib/thinking_sphinx/real_time/index.rb +20 -12
  38. data/lib/thinking_sphinx/search/glaze.rb +5 -0
  39. data/lib/thinking_sphinx/search/query.rb +4 -8
  40. data/lib/thinking_sphinx/tasks.rb +8 -0
  41. data/spec/acceptance/excerpts_spec.rb +22 -0
  42. data/spec/acceptance/remove_deleted_records_spec.rb +10 -0
  43. data/spec/acceptance/searching_across_models_spec.rb +10 -0
  44. data/spec/acceptance/searching_with_filters_spec.rb +15 -0
  45. data/spec/acceptance/specifying_sql_spec.rb +3 -3
  46. data/spec/acceptance/sphinx_scopes_spec.rb +11 -0
  47. data/spec/internal/app/indices/product_index.rb +2 -0
  48. data/spec/internal/app/models/categorisation.rb +6 -0
  49. data/spec/internal/app/models/category.rb +3 -0
  50. data/spec/internal/app/models/product.rb +4 -1
  51. data/spec/internal/db/schema.rb +10 -0
  52. data/spec/thinking_sphinx/active_record/base_spec.rb +33 -0
  53. data/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb +4 -35
  54. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +5 -10
  55. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +4 -3
  56. data/spec/thinking_sphinx/configuration_spec.rb +26 -6
  57. data/spec/thinking_sphinx/connection_spec.rb +4 -1
  58. data/spec/thinking_sphinx/deletion_spec.rb +76 -0
  59. data/spec/thinking_sphinx/facet_search_spec.rb +54 -5
  60. data/spec/thinking_sphinx/panes/excerpts_pane_spec.rb +4 -6
  61. data/spec/thinking_sphinx/rake_interface_spec.rb +35 -0
  62. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +68 -28
  63. data/spec/thinking_sphinx/search/glaze_spec.rb +19 -0
  64. data/spec/thinking_sphinx/search/query_spec.rb +39 -2
  65. data/thinking-sphinx.gemspec +2 -2
  66. metadata +31 -45
@@ -4,13 +4,16 @@ describe ThinkingSphinx::Connection do
4
4
  describe '.take' do
5
5
  let(:pool) { double 'pool' }
6
6
  let(:connection) { double 'connection' }
7
- let(:error) { Mysql2::Error.new '' }
7
+ let(:error) { ThinkingSphinx::QueryExecutionError.new 'failed' }
8
8
  let(:translated_error) { ThinkingSphinx::SphinxError.new }
9
9
 
10
10
  before :each do
11
11
  ThinkingSphinx::Connection.stub :pool => pool
12
12
  ThinkingSphinx::SphinxError.stub :new_from_mysql => translated_error
13
13
  pool.stub(:take).and_yield(connection)
14
+
15
+ error.statement = 'SELECT * FROM article_core'
16
+ translated_error.statement = 'SELECT * FROM article_core'
14
17
  end
15
18
 
16
19
  it "yields a connection from the pool" do
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe ThinkingSphinx::Deletion do
4
+ describe '.perform' do
5
+ let(:connection) { double('connection', :execute => nil) }
6
+ let(:index) { double('index', :name => 'foo_core',
7
+ :document_id_for_key => 14, :type => 'plain') }
8
+ let(:instance) { double('instance', :id => 7) }
9
+ let(:pool) { double 'pool' }
10
+
11
+ before :each do
12
+ ThinkingSphinx::Connection.stub :pool => pool
13
+ Riddle::Query.stub :update => 'UPDATE STATEMENT'
14
+
15
+ pool.stub(:take).and_yield(connection)
16
+ end
17
+
18
+ context 'index is SQL-backed' do
19
+ it "updates the deleted flag to false" do
20
+ connection.should_receive(:execute).with('UPDATE STATEMENT')
21
+
22
+ ThinkingSphinx::Deletion.perform index, instance
23
+ end
24
+
25
+ it "builds the update query for the given index" do
26
+ Riddle::Query.should_receive(:update).
27
+ with('foo_core', anything, anything).and_return('')
28
+
29
+ ThinkingSphinx::Deletion.perform index, instance
30
+ end
31
+
32
+ it "builds the update query for the sphinx document id" do
33
+ Riddle::Query.should_receive(:update).
34
+ with(anything, 14, anything).and_return('')
35
+
36
+ ThinkingSphinx::Deletion.perform index, instance
37
+ end
38
+
39
+ it "builds the update query for setting sphinx_deleted to true" do
40
+ Riddle::Query.should_receive(:update).
41
+ with(anything, anything, :sphinx_deleted => true).and_return('')
42
+
43
+ ThinkingSphinx::Deletion.perform index, instance
44
+ end
45
+
46
+ it "doesn't care about Sphinx errors" do
47
+ connection.stub(:execute).and_raise(Mysql2::Error.new(''))
48
+
49
+ lambda {
50
+ ThinkingSphinx::Deletion.perform index, instance
51
+ }.should_not raise_error
52
+ end
53
+ end
54
+
55
+ context "index is real-time" do
56
+ before :each do
57
+ index.stub :type => 'rt'
58
+ end
59
+
60
+ it "deletes the record to false" do
61
+ connection.should_receive(:execute).
62
+ with('DELETE FROM foo_core WHERE id = 14')
63
+
64
+ ThinkingSphinx::Deletion.perform index, instance
65
+ end
66
+
67
+ it "doesn't care about Sphinx errors" do
68
+ connection.stub(:execute).and_raise(Mysql2::Error.new(''))
69
+
70
+ lambda {
71
+ ThinkingSphinx::Deletion.perform index, instance
72
+ }.should_not raise_error
73
+ end
74
+ end
75
+ end
76
+ end
@@ -7,15 +7,21 @@ describe ThinkingSphinx::FacetSearch do
7
7
  let(:facet_search) { ThinkingSphinx::FacetSearch.new '', {} }
8
8
  let(:batch) { double('batch', :searches => [], :populate => true) }
9
9
  let(:index_set) { [] }
10
- let(:index) { double('index', :facets => [property],
10
+ let(:index) { double('index', :facets => [property_a, property_b],
11
11
  :name => 'foo_core') }
12
- let(:property) { double('property', :name => 'price_bracket',
13
- :multi? => false)}
12
+ let(:property_a) { double('property', :name => 'price_bracket',
13
+ :multi? => false) }
14
+ let(:property_b) { double('property', :name => 'category_id',
15
+ :multi? => false) }
16
+ let(:configuration) { double 'configuration', :settings => {} }
14
17
 
15
18
  before :each do
16
19
  stub_const 'ThinkingSphinx::IndexSet', double(:new => index_set)
17
20
  stub_const 'ThinkingSphinx::BatchedSearch', double(:new => batch)
18
21
  stub_const 'ThinkingSphinx::Search', DumbSearch
22
+ stub_const 'ThinkingSphinx::Middlewares::RAW_ONLY', double
23
+ stub_const 'ThinkingSphinx::Configuration',
24
+ double(:instance => configuration)
19
25
 
20
26
  index_set << index << double('index', :facets => [], :name => 'bar_core')
21
27
  end
@@ -26,6 +32,7 @@ describe ThinkingSphinx::FacetSearch do
26
32
  'sphinx_internal_class' => 'Foo',
27
33
  'price_bracket' => 3,
28
34
  'tag_ids' => '1,2',
35
+ 'category_id' => 11,
29
36
  '@count' => 5,
30
37
  '@groupby' => 2
31
38
  }]
@@ -55,8 +62,18 @@ describe ThinkingSphinx::FacetSearch do
55
62
  }.should_not be_nil
56
63
  end
57
64
 
65
+ it "limits facets to the specified set" do
66
+ facet_search.options[:facets] = [:category_id]
67
+
68
+ facet_search.populate
69
+
70
+ batch.searches.collect { |search|
71
+ search.options[:group_by]
72
+ }.should == ['category_id']
73
+ end
74
+
58
75
  it "aliases the class facet from sphinx_internal_class" do
59
- property.stub :name => 'sphinx_internal_class'
76
+ property_a.stub :name => 'sphinx_internal_class'
60
77
 
61
78
  facet_search.populate
62
79
 
@@ -64,11 +81,43 @@ describe ThinkingSphinx::FacetSearch do
64
81
  end
65
82
 
66
83
  it "uses the @groupby value for MVAs" do
67
- property.stub :name => 'tag_ids', :multi? => true
84
+ property_a.stub :name => 'tag_ids', :multi? => true
68
85
 
69
86
  facet_search.populate
70
87
 
71
88
  facet_search[:tag_ids].should == {2 => 5}
72
89
  end
90
+
91
+ [:max_matches, :limit].each do |setting|
92
+ it "sets #{setting} in each search" do
93
+ facet_search.populate
94
+
95
+ batch.searches.each { |search|
96
+ search.options[setting].should == 1000
97
+ }
98
+ end
99
+
100
+ it "respects configured max_matches values for #{setting}" do
101
+ configuration.settings['max_matches'] = 1234
102
+
103
+ facet_search.populate
104
+
105
+ batch.searches.each { |search|
106
+ search.options[setting].should == 1234
107
+ }
108
+ end
109
+ end
110
+
111
+ [:limit, :per_page].each do |setting|
112
+ it "respects #{setting} option if set" do
113
+ facet_search = ThinkingSphinx::FacetSearch.new '', {setting => 42}
114
+
115
+ facet_search.populate
116
+
117
+ batch.searches.each { |search|
118
+ search.options[setting].should == 42
119
+ }
120
+ end
121
+ end
73
122
  end
74
123
  end
@@ -7,11 +7,10 @@ require 'thinking_sphinx/panes/excerpts_pane'
7
7
  describe ThinkingSphinx::Panes::ExcerptsPane do
8
8
  let(:pane) {
9
9
  ThinkingSphinx::Panes::ExcerptsPane.new context, object, raw }
10
- let(:context) { {:indices => [double(:name => 'foo_core')],
11
- :meta => {}} }
10
+ let(:context) { {:indices => [double(:name => 'foo_core')]} }
12
11
  let(:object) { double('object') }
13
12
  let(:raw) { {} }
14
- let(:search) { double('search', :options => {}) }
13
+ let(:search) { double('search', :query => 'foo', :options => {}) }
15
14
 
16
15
  before :each do
17
16
  context.stub :search => search
@@ -30,10 +29,9 @@ describe ThinkingSphinx::Panes::ExcerptsPane do
30
29
  pane.excerpts.should == excerpts
31
30
  end
32
31
 
33
- it "creates an excerpter with the first index and all keywords" do
32
+ it "creates an excerpter with the first index and the query and conditions values" do
34
33
  context[:indices] = [double(:name => 'alpha'), double(:name => 'beta')]
35
- context[:meta]['keyword[0]'] = 'foo'
36
- context[:meta]['keyword[1]'] = 'bar'
34
+ context.search.options[:conditions] = {:baz => 'bar'}
37
35
 
38
36
  ThinkingSphinx::Excerpter.should_receive(:new).
39
37
  with('alpha', 'foo bar', anything).and_return(excerpter)
@@ -9,6 +9,32 @@ describe ThinkingSphinx::RakeInterface do
9
9
  interface.stub(:puts => nil)
10
10
  end
11
11
 
12
+ describe '#clear' do
13
+ let(:controller) { double 'controller' }
14
+
15
+ before :each do
16
+ configuration.stub(
17
+ :indices_location => '/path/to/indices',
18
+ :searchd => double(:binlog_path => '/path/to/binlog')
19
+ )
20
+
21
+ FileUtils.stub :rm_r => true
22
+ File.stub :exists? => true
23
+ end
24
+
25
+ it "removes the directory for the index files" do
26
+ FileUtils.should_receive(:rm_r).with('/path/to/indices')
27
+
28
+ interface.clear
29
+ end
30
+
31
+ it "removes the directory for the binlog files" do
32
+ FileUtils.should_receive(:rm_r).with('/path/to/binlog')
33
+
34
+ interface.clear
35
+ end
36
+ end
37
+
12
38
  describe '#configure' do
13
39
  let(:controller) { double('controller') }
14
40
 
@@ -86,6 +112,15 @@ describe ThinkingSphinx::RakeInterface do
86
112
 
87
113
  before :each do
88
114
  controller.stub(:running?).and_return(false, true)
115
+ configuration.stub :indices_location => 'my/index/files'
116
+
117
+ FileUtils.stub :mkdir_p => true
118
+ end
119
+
120
+ it "creates the index files directory" do
121
+ FileUtils.should_receive(:mkdir_p).with('my/index/files')
122
+
123
+ interface.start
89
124
  end
90
125
 
91
126
  it "starts the daemon" do
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks do
4
4
  let(:callbacks) {
5
- ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new instance
5
+ ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new :article
6
6
  }
7
7
  let(:instance) { double('instance', :id => 12) }
8
8
  let(:config) { double('config', :indices_for_references => [index]) }
@@ -16,30 +16,6 @@ describe ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks do
16
16
  ThinkingSphinx::Connection.stub_chain(:pool, :take).and_yield connection
17
17
  end
18
18
 
19
- describe '.after_save' do
20
- let(:callbacks) { double('callbacks', :after_save => nil) }
21
-
22
- before :each do
23
- ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.
24
- stub :new => callbacks
25
- end
26
-
27
- it "builds an object from the instance" do
28
- ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.
29
- should_receive(:new).with(instance).and_return(callbacks)
30
-
31
- ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.
32
- after_save(instance)
33
- end
34
-
35
- it "invokes after_save on the object" do
36
- callbacks.should_receive(:after_save)
37
-
38
- ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.
39
- after_save(instance)
40
- end
41
- end
42
-
43
19
  describe '#after_save' do
44
20
  let(:insert) { double('insert', :to_sql => 'REPLACE INTO my_index') }
45
21
  let(:time) { 1.day.ago }
@@ -59,19 +35,83 @@ describe ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks do
59
35
  with('my_index', ['id', 'name', 'created_at'], [123, 'Foo', time]).
60
36
  and_return(insert)
61
37
 
62
- callbacks.after_save
38
+ callbacks.after_save instance
63
39
  end
64
40
 
65
41
  it "switches the insert to a replace statement" do
66
42
  insert.should_receive(:replace!).and_return(insert)
67
43
 
68
- callbacks.after_save
44
+ callbacks.after_save instance
69
45
  end
70
46
 
71
47
  it "sends the insert through to the server" do
72
48
  connection.should_receive(:execute).with('REPLACE INTO my_index')
73
49
 
74
- callbacks.after_save
50
+ callbacks.after_save instance
51
+ end
52
+
53
+ context 'with a given path' do
54
+ let(:callbacks) {
55
+ ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new(
56
+ :article, [:user]
57
+ )
58
+ }
59
+ let(:instance) { double('instance', :id => 12, :user => user) }
60
+ let(:user) { double('user', :id => 13) }
61
+
62
+ it "creates an insert statement with all fields and attributes" do
63
+ Riddle::Query::Insert.should_receive(:new).
64
+ with('my_index', ['id', 'name', 'created_at'], [123, 'Foo', time]).
65
+ and_return(insert)
66
+
67
+ callbacks.after_save instance
68
+ end
69
+
70
+ it "gets the document id for the user object" do
71
+ index.should_receive(:document_id_for_key).with(13).and_return(123)
72
+
73
+ callbacks.after_save instance
74
+ end
75
+
76
+ it "translates values for the user object" do
77
+ field.should_receive(:translate).with(user).and_return('Foo')
78
+
79
+ callbacks.after_save instance
80
+ end
81
+ end
82
+
83
+ context 'with a path returning multiple objects' do
84
+ let(:callbacks) {
85
+ ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new(
86
+ :article, [:readers]
87
+ )
88
+ }
89
+ let(:instance) { double('instance', :id => 12,
90
+ :readers => [user_a, user_b]) }
91
+ let(:user_a) { double('user', :id => 13) }
92
+ let(:user_b) { double('user', :id => 14) }
93
+
94
+ it "creates insert statements with all fields and attributes" do
95
+ Riddle::Query::Insert.should_receive(:new).twice.
96
+ with('my_index', ['id', 'name', 'created_at'], [123, 'Foo', time]).
97
+ and_return(insert)
98
+
99
+ callbacks.after_save instance
100
+ end
101
+
102
+ it "gets the document id for each reader" do
103
+ index.should_receive(:document_id_for_key).with(13).and_return(123)
104
+ index.should_receive(:document_id_for_key).with(14).and_return(123)
105
+
106
+ callbacks.after_save instance
107
+ end
108
+
109
+ it "translates values for each reader" do
110
+ field.should_receive(:translate).with(user_a).and_return('Foo')
111
+ field.should_receive(:translate).with(user_b).and_return('Foo')
112
+
113
+ callbacks.after_save instance
114
+ end
75
115
  end
76
116
  end
77
117
  end
@@ -50,6 +50,25 @@ describe ThinkingSphinx::Search::Glaze do
50
50
  end
51
51
  end
52
52
 
53
+ describe '#respond_to?' do
54
+ it "responds to underlying object methods" do
55
+ object.stub :foo => true
56
+
57
+ glaze.respond_to?(:foo).should be_true
58
+ end
59
+
60
+ it "responds to underlying pane methods" do
61
+ pane = double('Pane Class', :new => double('pane', :bar => true))
62
+ glaze = ThinkingSphinx::Search::Glaze.new context, object, raw, [pane]
63
+
64
+ glaze.respond_to?(:bar).should be_true
65
+ end
66
+
67
+ it "does not to respond to methods that don't exist" do
68
+ glaze.respond_to?(:something).should be_false
69
+ end
70
+ end
71
+
53
72
  describe '#unglazed' do
54
73
  it "returns the original object" do
55
74
  glaze.unglazed.should == object
@@ -39,9 +39,34 @@ describe ThinkingSphinx::Search::Query do
39
39
 
40
40
  it "does not star the sphinx_internal_class field keyword" do
41
41
  query = ThinkingSphinx::Search::Query.new '',
42
- {:sphinx_internal_class => 'article'}, true
42
+ {:sphinx_internal_class_name => 'article'}, true
43
43
 
44
- query.to_s.should == '@sphinx_internal_class article'
44
+ query.to_s.should == '@sphinx_internal_class_name article'
45
+ end
46
+
47
+ it "treats escapes as word characters" do
48
+ query = ThinkingSphinx::Search::Query.new '', {:title => 'sauce\\@pan'},
49
+ true
50
+
51
+ query.to_s.should == '@title *sauce\\@pan*'
52
+ end
53
+
54
+ it "does not star manually provided field tags" do
55
+ query = ThinkingSphinx::Search::Query.new "@title pan", {}, true
56
+
57
+ query.to_s.should == "@title *pan*"
58
+ end
59
+
60
+ it "does not star manually provided arrays of field tags" do
61
+ query = ThinkingSphinx::Search::Query.new "@(title, body) pan", {}, true
62
+
63
+ query.to_s.should == "@(title, body) *pan*"
64
+ end
65
+
66
+ it "stars keywords that begin with an escaped @" do
67
+ query = ThinkingSphinx::Search::Query.new "\\@pan", {}, true
68
+
69
+ query.to_s.should == "*\\@pan*"
45
70
  end
46
71
 
47
72
  it "handles null values by removing them from the conditions hash" do
@@ -56,6 +81,18 @@ describe ThinkingSphinx::Search::Query do
56
81
  query.to_s.should == ''
57
82
  end
58
83
 
84
+ it "handles nil queries" do
85
+ query = ThinkingSphinx::Search::Query.new nil, {}
86
+
87
+ query.to_s.should == ''
88
+ end
89
+
90
+ it "handles nil queries when starring" do
91
+ query = ThinkingSphinx::Search::Query.new nil, {}, true
92
+
93
+ query.to_s.should == ''
94
+ end
95
+
59
96
  it "allows mixing of blank and non-blank conditions" do
60
97
  query = ThinkingSphinx::Search::Query.new 'tasty', :title => 'pancakes',
61
98
  :ingredients => nil