thinking-sphinx 3.0.5 → 3.0.6

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