thinking-sphinx 3.0.5 → 3.0.6

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/HISTORY +25 -0
  4. data/README.textile +2 -2
  5. data/lib/thinking_sphinx.rb +5 -0
  6. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +1 -1
  7. data/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +1 -1
  8. data/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb +1 -1
  9. data/lib/thinking_sphinx/active_record/property_sql_presenter.rb +10 -2
  10. data/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb +6 -7
  11. data/lib/thinking_sphinx/active_record/sql_builder/query.rb +8 -4
  12. data/lib/thinking_sphinx/active_record/sql_builder/statement.rb +7 -4
  13. data/lib/thinking_sphinx/active_record/sql_source.rb +1 -1
  14. data/lib/thinking_sphinx/configuration.rb +8 -3
  15. data/lib/thinking_sphinx/connection.rb +2 -0
  16. data/lib/thinking_sphinx/deletion.rb +1 -1
  17. data/lib/thinking_sphinx/deltas.rb +1 -1
  18. data/lib/thinking_sphinx/deltas/delete_job.rb +1 -1
  19. data/lib/thinking_sphinx/errors.rb +8 -0
  20. data/lib/thinking_sphinx/excerpter.rb +2 -2
  21. data/lib/thinking_sphinx/facet.rb +2 -2
  22. data/lib/thinking_sphinx/facet_search.rb +2 -1
  23. data/lib/thinking_sphinx/float_formatter.rb +33 -0
  24. data/lib/thinking_sphinx/index_set.rb +2 -4
  25. data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -3
  26. data/lib/thinking_sphinx/masks/scopes_mask.rb +5 -0
  27. data/lib/thinking_sphinx/masks/weight_enumerator_mask.rb +1 -1
  28. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +6 -1
  29. data/lib/thinking_sphinx/middlewares/geographer.rb +10 -4
  30. data/lib/thinking_sphinx/middlewares/sphinxql.rb +14 -11
  31. data/lib/thinking_sphinx/middlewares/utf8.rb +2 -3
  32. data/lib/thinking_sphinx/panes/weight_pane.rb +1 -1
  33. data/lib/thinking_sphinx/rake_interface.rb +9 -7
  34. data/lib/thinking_sphinx/real_time/interpreter.rb +18 -0
  35. data/lib/thinking_sphinx/real_time/property.rb +4 -2
  36. data/lib/thinking_sphinx/search.rb +11 -10
  37. data/lib/thinking_sphinx/sphinxql.rb +17 -0
  38. data/lib/thinking_sphinx/tasks.rb +5 -1
  39. data/lib/thinking_sphinx/utf8.rb +16 -0
  40. data/spec/acceptance/attribute_access_spec.rb +4 -2
  41. data/spec/acceptance/searching_within_a_model_spec.rb +6 -0
  42. data/spec/acceptance/sorting_search_results_spec.rb +7 -0
  43. data/spec/acceptance/support/sphinx_controller.rb +5 -0
  44. data/spec/internal/app/indices/city_index.rb +1 -0
  45. data/spec/internal/app/indices/product_index.rb +1 -1
  46. data/spec/internal/tmp/.gitkeep +0 -0
  47. data/spec/thinking_sphinx/active_record/base_spec.rb +10 -0
  48. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +3 -2
  49. data/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb +1 -1
  50. data/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb +2 -2
  51. data/spec/thinking_sphinx/active_record/field_spec.rb +13 -0
  52. data/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb +13 -0
  53. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +62 -2
  54. data/spec/thinking_sphinx/configuration_spec.rb +1 -1
  55. data/spec/thinking_sphinx/deletion_spec.rb +4 -2
  56. data/spec/thinking_sphinx/deltas/default_delta_spec.rb +2 -1
  57. data/spec/thinking_sphinx/deltas_spec.rb +17 -6
  58. data/spec/thinking_sphinx/errors_spec.rb +7 -0
  59. data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
  60. data/spec/thinking_sphinx/masks/scopes_mask_spec.rb +64 -0
  61. data/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb +17 -1
  62. data/spec/thinking_sphinx/middlewares/geographer_spec.rb +11 -0
  63. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +11 -0
  64. data/spec/thinking_sphinx/panes/weight_pane_spec.rb +1 -1
  65. data/spec/thinking_sphinx/rake_interface_spec.rb +6 -0
  66. data/spec/thinking_sphinx/real_time/field_spec.rb +13 -0
  67. data/spec/thinking_sphinx/real_time/interpreter_spec.rb +40 -0
  68. data/thinking-sphinx.gemspec +3 -2
  69. metadata +12 -8
  70. data/spec/internal/.gitignore +0 -1
@@ -1,5 +1,5 @@
1
1
  ThinkingSphinx::Index.define :product, :with => :real_time do
2
- indexes name
2
+ indexes name, :sortable => true
3
3
 
4
4
  has category_ids, :type => :integer, :multi => true
5
5
  end
File without changes
@@ -65,6 +65,16 @@ describe ThinkingSphinx::ActiveRecord::Base do
65
65
  options[:classes].should == [sub_model, model]
66
66
  end
67
67
 
68
+ it "respects provided middleware" do
69
+ model.search(:middleware => ThinkingSphinx::Middlewares::RAW_ONLY).
70
+ options[:middleware].should == ThinkingSphinx::Middlewares::RAW_ONLY
71
+ end
72
+
73
+ it "respects provided masks" do
74
+ model.search(:masks => [ThinkingSphinx::Masks::PaginationMask]).
75
+ masks.should == [ThinkingSphinx::Masks::PaginationMask]
76
+ end
77
+
68
78
  it "applies the default scope if there is one" do
69
79
  model.stub :default_sphinx_scope => :default,
70
80
  :sphinx_scopes => {:default => Proc.new { {:order => :created_at} }}
@@ -5,8 +5,8 @@ module ThinkingSphinx
5
5
  end
6
6
 
7
7
  require 'active_support/core_ext/string/inflections'
8
- require 'mysql2/error'
9
8
  require 'thinking_sphinx/callbacks'
9
+ require 'thinking_sphinx/errors'
10
10
  require 'thinking_sphinx/active_record/callbacks/update_callbacks'
11
11
 
12
12
  describe ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks do
@@ -64,7 +64,8 @@ describe ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks do
64
64
  end
65
65
 
66
66
  it "doesn't care if the update fails at Sphinx's end" do
67
- connection.stub(:execute).and_raise(Mysql2::Error.new(''))
67
+ connection.stub(:execute).
68
+ and_raise(ThinkingSphinx::ConnectionError.new(''))
68
69
 
69
70
  lambda { callbacks.after_update }.should_not raise_error
70
71
  end
@@ -43,7 +43,7 @@ describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter do
43
43
  describe '#group_concatenate' do
44
44
  it "group concatenates the clause with the given separator" do
45
45
  adapter.group_concatenate('foo', ',').
46
- should == "GROUP_CONCAT(foo SEPARATOR ',')"
46
+ should == "GROUP_CONCAT(DISTINCT foo SEPARATOR ',')"
47
47
  end
48
48
  end
49
49
  end
@@ -30,7 +30,7 @@ describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter do
30
30
 
31
31
  it "converts to bigint unix timestamps" do
32
32
  ThinkingSphinx::Configuration.instance.settings['64bit_timestamps'] = true
33
-
33
+
34
34
  adapter.cast_to_timestamp('created_at').
35
35
  should == 'extract(epoch from created_at)::bigint'
36
36
  end
@@ -52,7 +52,7 @@ describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter do
52
52
  describe '#group_concatenate' do
53
53
  it "group concatenates the clause with the given separator" do
54
54
  adapter.group_concatenate('foo', ',').
55
- should == "array_to_string(array_agg(foo), ',')"
55
+ should == "array_to_string(array_agg(DISTINCT foo), ',')"
56
56
  end
57
57
  end
58
58
  end
@@ -10,6 +10,19 @@ describe ThinkingSphinx::ActiveRecord::Field do
10
10
  column.stub! :to_a => [column]
11
11
  end
12
12
 
13
+ describe '#columns' do
14
+ it 'returns the provided Column object' do
15
+ field.columns.should == [column]
16
+ end
17
+
18
+ it 'translates symbols to Column objects' do
19
+ ThinkingSphinx::ActiveRecord::Column.should_receive(:new).with(:title).
20
+ and_return(column)
21
+
22
+ ThinkingSphinx::ActiveRecord::Field.new model, :title
23
+ end
24
+ end
25
+
13
26
  describe '#file?' do
14
27
  it "defaults to false" do
15
28
  field.should_not be_file
@@ -222,6 +222,19 @@ describe ThinkingSphinx::ActiveRecord::PropertySQLPresenter do
222
222
  presenter.to_select.should == "CONCAT_WS(',', CAST(articles.created_at AS varchar), CAST(articles.created_at AS varchar)) AS created_at"
223
223
  end
224
224
 
225
+ it "double-casts and concatenates multiple columns for timestamp attributes" do
226
+ adapter.stub :concatenate do |clause, separator|
227
+ "CONCAT_WS('#{separator}', #{clause})"
228
+ end
229
+ adapter.stub :cast_to_string do |clause|
230
+ "CAST(#{clause} AS varchar)"
231
+ end
232
+
233
+ attribute.stub :columns => [column, column], :type => :timestamp
234
+
235
+ presenter.to_select.should == "CONCAT_WS(',', CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar), CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar)) AS created_at"
236
+ end
237
+
225
238
  it "returns nil for query sourced attributes" do
226
239
  attribute.stub :source_type => :query
227
240
 
@@ -4,14 +4,15 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
4
4
  let(:source) { double('source', :model => model, :offset => 3,
5
5
  :fields => [], :attributes => [], :disable_range? => false,
6
6
  :delta_processor => nil, :conditions => [], :groupings => [],
7
- :adapter => adapter, :associations => [], :primary_key => :id) }
7
+ :adapter => adapter, :associations => [], :primary_key => :id,
8
+ :options => {}) }
8
9
  let(:model) { double('model', :connection => connection,
9
10
  :descends_from_active_record? => true, :column_names => [],
10
11
  :inheritance_column => 'type', :unscoped => relation,
11
12
  :quoted_table_name => '`users`', :name => 'User') }
12
13
  let(:connection) { double('connection') }
13
14
  let(:relation) { double('relation') }
14
- let(:config) { double('config', :indices => indices) }
15
+ let(:config) { double('config', :indices => indices, :settings => {}) }
15
16
  let(:indices) { double('indices', :count => 5) }
16
17
  let(:presenter) { double('presenter', :to_select => '`name` AS `name`',
17
18
  :to_group => '`name`') }
@@ -395,6 +396,55 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
395
396
  builder.sql_query
396
397
  end
397
398
 
399
+ context 'group by shortcut' do
400
+ before :each do
401
+ source.options[:minimal_group_by?] = true
402
+ end
403
+
404
+ it "groups by the primary key" do
405
+ relation.should_receive(:group) do |string|
406
+ string.should match(/"users"."id"/)
407
+ relation
408
+ end
409
+
410
+ builder.sql_query
411
+ end
412
+
413
+ it "does not group by fields" do
414
+ source.fields << double('field')
415
+
416
+ relation.should_receive(:group) do |string|
417
+ string.should_not match(/"name"/)
418
+ relation
419
+ end
420
+
421
+ builder.sql_query
422
+ end
423
+
424
+ it "does not group by attributes" do
425
+ source.attributes << double('attribute')
426
+ presenter.stub!(:to_group => '"created_at"')
427
+
428
+ relation.should_receive(:group) do |string|
429
+ string.should_not match(/"created_at"/)
430
+ relation
431
+ end
432
+
433
+ builder.sql_query
434
+ end
435
+
436
+ it "groups by source groupings" do
437
+ source.groupings << '"latitude"'
438
+
439
+ relation.should_receive(:group) do |string|
440
+ string.should match(/"latitude"/)
441
+ relation
442
+ end
443
+
444
+ builder.sql_query
445
+ end
446
+ end
447
+
398
448
  context 'STI model' do
399
449
  before :each do
400
450
  model.column_names << 'type'
@@ -546,6 +596,16 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
546
596
 
547
597
  builder.sql_query_pre.should_not include('SET UTF8')
548
598
  end
599
+
600
+ it "adds a time-zone query by default" do
601
+ expect(builder.sql_query_pre).to include('SET TIME ZONE')
602
+ end
603
+
604
+ it "does not add a time-zone query if requested" do
605
+ config.settings['skip_time_zone'] = true
606
+
607
+ expect(builder.sql_query_pre).to_not include('SET TIME ZONE')
608
+ end
549
609
  end
550
610
 
551
611
  describe 'sql_query_range' do
@@ -40,7 +40,7 @@ describe ThinkingSphinx::Configuration do
40
40
  config = ThinkingSphinx::Configuration.instance
41
41
  config.settings['foo'].should == 'bugs'
42
42
 
43
- config.framework = double :environment => 'production', :root => '/tmp/'
43
+ config.framework = double :environment => 'production', :root => Pathname.new(__FILE__).join('..', '..', 'internal')
44
44
  config.settings['foo'].should == 'bar'
45
45
  end
46
46
  end
@@ -41,7 +41,8 @@ describe ThinkingSphinx::Deletion do
41
41
  end
42
42
 
43
43
  it "doesn't care about Sphinx errors" do
44
- connection.stub(:execute).and_raise(Mysql2::Error.new(''))
44
+ connection.stub(:execute).
45
+ and_raise(ThinkingSphinx::ConnectionError.new(''))
45
46
 
46
47
  lambda {
47
48
  ThinkingSphinx::Deletion.perform index, instance
@@ -62,7 +63,8 @@ describe ThinkingSphinx::Deletion do
62
63
  end
63
64
 
64
65
  it "doesn't care about Sphinx errors" do
65
- connection.stub(:execute).and_raise(Mysql2::Error.new(''))
66
+ connection.stub(:execute).
67
+ and_raise(ThinkingSphinx::ConnectionError.new(''))
66
68
 
67
69
  lambda {
68
70
  ThinkingSphinx::Deletion.perform index, instance
@@ -57,7 +57,8 @@ describe ThinkingSphinx::Deltas::DefaultDelta do
57
57
  end
58
58
 
59
59
  it "doesn't care about Sphinx errors" do
60
- connection.stub(:execute).and_raise(Mysql2::Error.new(''))
60
+ connection.stub(:execute).
61
+ and_raise(ThinkingSphinx::ConnectionError.new(''))
61
62
 
62
63
  lambda { delta.delete index, instance }.should_not raise_error
63
64
  end
@@ -11,7 +11,7 @@ describe ThinkingSphinx::Deltas do
11
11
  klass = Class.new
12
12
  ThinkingSphinx::Deltas.processor_for(klass).should == klass
13
13
  end
14
-
14
+
15
15
  it "instantiates a class from the name as a string" do
16
16
  ThinkingSphinx::Deltas.
17
17
  processor_for('ThinkingSphinx::Deltas::DefaultDelta').
@@ -20,10 +20,13 @@ describe ThinkingSphinx::Deltas do
20
20
  end
21
21
 
22
22
  describe '.suspend' do
23
- let(:config) { double('config', :indices_for_references => [index]) }
24
- let(:index) { double('index', :name => 'user_core',
25
- :delta_processor => processor) }
26
- let(:processor) { double('processor', :index => true) }
23
+ let(:config) { double('config',
24
+ :indices_for_references => [core_index, delta_index]) }
25
+ let(:core_index) { double('index', :name => 'user_core',
26
+ :delta_processor => processor, :delta? => false) }
27
+ let(:delta_index) { double('index', :name => 'user_core',
28
+ :delta_processor => processor, :delta? => true) }
29
+ let(:processor) { double('processor', :index => true) }
27
30
 
28
31
  before :each do
29
32
  ThinkingSphinx::Configuration.stub :instance => config
@@ -54,7 +57,15 @@ describe ThinkingSphinx::Deltas do
54
57
  end
55
58
 
56
59
  it "processes the delta indices for the given reference" do
57
- processor.should_receive(:index).with(index)
60
+ processor.should_receive(:index).with(delta_index)
61
+
62
+ ThinkingSphinx::Deltas.suspend :user do
63
+ #
64
+ end
65
+ end
66
+
67
+ it "does not process the core indices for the given reference" do
68
+ processor.should_not_receive(:index).with(core_index)
58
69
 
59
70
  ThinkingSphinx::Deltas.suspend :user do
60
71
  #
@@ -26,6 +26,13 @@ describe ThinkingSphinx::SphinxError do
26
26
  should be_a(ThinkingSphinx::QueryError)
27
27
  end
28
28
 
29
+ it "translates connection errors" do
30
+ error.stub :message => "Can't connect to MySQL server on '127.0.0.1' (61)"
31
+
32
+ ThinkingSphinx::SphinxError.new_from_mysql(error).
33
+ should be_a(ThinkingSphinx::ConnectionError)
34
+ end
35
+
29
36
  it "defaults to sphinx errors" do
30
37
  error.stub :message => 'index foo: unknown error: something is wrong'
31
38
 
@@ -29,12 +29,12 @@ describe ThinkingSphinx::FacetSearch do
29
29
  DumbSearch = ::Struct.new(:query, :options) do
30
30
  def raw
31
31
  [{
32
- 'sphinx_internal_class' => 'Foo',
33
- 'price_bracket' => 3,
34
- 'tag_ids' => '1,2',
35
- 'category_id' => 11,
36
- '@count' => 5,
37
- '@groupby' => 2
32
+ 'sphinx_internal_class' => 'Foo',
33
+ 'price_bracket' => 3,
34
+ 'tag_ids' => '1,2',
35
+ 'category_id' => 11,
36
+ ThinkingSphinx::SphinxQL.count => 5,
37
+ ThinkingSphinx::SphinxQL.group_by => 2
38
38
  }]
39
39
  end
40
40
  end
@@ -65,4 +65,68 @@ describe ThinkingSphinx::Masks::ScopesMask do
65
65
  mask.search.object_id.should == search.object_id
66
66
  end
67
67
  end
68
+
69
+ describe '#search_for_ids' do
70
+ it "replaces the query if one is supplied" do
71
+ search.should_receive(:query=).with('bar')
72
+
73
+ mask.search_for_ids('bar')
74
+ end
75
+
76
+ it "keeps the existing query when only options are offered" do
77
+ search.should_not_receive(:query=)
78
+
79
+ mask.search_for_ids :with => {:foo => :bar}
80
+ end
81
+
82
+ it "merges conditions" do
83
+ search.options[:conditions] = {:foo => 'bar'}
84
+
85
+ mask.search_for_ids :conditions => {:baz => 'qux'}
86
+
87
+ search.options[:conditions].should == {:foo => 'bar', :baz => 'qux'}
88
+ end
89
+
90
+ it "merges filters" do
91
+ search.options[:with] = {:foo => :bar}
92
+
93
+ mask.search_for_ids :with => {:baz => :qux}
94
+
95
+ search.options[:with].should == {:foo => :bar, :baz => :qux}
96
+ end
97
+
98
+ it "merges exclusive filters" do
99
+ search.options[:without] = {:foo => :bar}
100
+
101
+ mask.search_for_ids :without => {:baz => :qux}
102
+
103
+ search.options[:without].should == {:foo => :bar, :baz => :qux}
104
+ end
105
+
106
+ it "appends excluded ids" do
107
+ search.options[:without_ids] = [1, 3]
108
+
109
+ mask.search_for_ids :without_ids => [5, 7]
110
+
111
+ search.options[:without_ids].should == [1, 3, 5, 7]
112
+ end
113
+
114
+ it "replaces the retry_stale option" do
115
+ search.options[:retry_stale] = true
116
+
117
+ mask.search_for_ids :retry_stale => 6
118
+
119
+ search.options[:retry_stale].should == 6
120
+ end
121
+
122
+ it "adds the ids_only option" do
123
+ mask.search_for_ids
124
+
125
+ search.options[:ids_only].should be_true
126
+ end
127
+
128
+ it "returns the original search object" do
129
+ mask.search_for_ids.object_id.should == search.object_id
130
+ end
131
+ end
68
132
  end
@@ -73,7 +73,7 @@ describe ThinkingSphinx::Middlewares::ActiveRecordTranslator do
73
73
  it "sorts the results according to Sphinx order, not database order" do
74
74
  model_name = double('article', :constantize => model)
75
75
  instance_1 = double('instance 1', :id => 1)
76
- instance_2 = double('instance 1', :id => 2)
76
+ instance_2 = double('instance 2', :id => 2)
77
77
 
78
78
  context[:results] << raw_result(2, model_name)
79
79
  context[:results] << raw_result(1, model_name)
@@ -85,6 +85,22 @@ describe ThinkingSphinx::Middlewares::ActiveRecordTranslator do
85
85
  context[:results].should == [instance_2, instance_1]
86
86
  end
87
87
 
88
+ it "returns objects in database order if a SQL order clause is supplied" do
89
+ model_name = double('article', :constantize => model)
90
+ instance_1 = double('instance 1', :id => 1)
91
+ instance_2 = double('instance 2', :id => 2)
92
+
93
+ context[:results] << raw_result(2, model_name)
94
+ context[:results] << raw_result(1, model_name)
95
+
96
+ model.stub(:order => model, :where => [instance_1, instance_2])
97
+ search.options[:sql] = {:order => 'name DESC'}
98
+
99
+ middleware.call [context]
100
+
101
+ context[:results].should == [instance_1, instance_2]
102
+ end
103
+
88
104
  context 'SQL options' do
89
105
  let(:relation) { double('relation', :where => []) }
90
106
 
@@ -4,6 +4,7 @@ end
4
4
 
5
5
  require 'thinking_sphinx/middlewares/middleware'
6
6
  require 'thinking_sphinx/middlewares/geographer'
7
+ require 'thinking_sphinx/float_formatter'
7
8
 
8
9
  describe ThinkingSphinx::Middlewares::Geographer do
9
10
  let(:app) { double('app', :call => true) }
@@ -84,6 +85,16 @@ describe ThinkingSphinx::Middlewares::Geographer do
84
85
 
85
86
  middleware.call [context]
86
87
  end
88
+
89
+ it "handles very small values" do
90
+ search.options[:geo] = [0.0000001, 0.00000000002]
91
+
92
+ sphinx_sql.should_receive(:values).
93
+ with('GEODIST(0.0000001, 0.00000000002, lat, lng) AS geodist').
94
+ and_return(sphinx_sql)
95
+
96
+ middleware.call [context]
97
+ end
87
98
  end
88
99
  end
89
100
  end