thinking-sphinx 3.1.4 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +7 -2
  3. data/Appraisals +4 -4
  4. data/Gemfile +2 -0
  5. data/HISTORY +24 -0
  6. data/README.textile +5 -4
  7. data/gemfiles/rails_3_2.gemfile +2 -1
  8. data/gemfiles/rails_4_0.gemfile +2 -1
  9. data/gemfiles/rails_4_1.gemfile +2 -1
  10. data/gemfiles/rails_4_2.gemfile +2 -1
  11. data/lib/thinking_sphinx.rb +4 -1
  12. data/lib/thinking_sphinx/active_record.rb +1 -0
  13. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +1 -1
  14. data/lib/thinking_sphinx/active_record/attribute/type.rb +1 -1
  15. data/lib/thinking_sphinx/active_record/base.rb +1 -1
  16. data/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +1 -1
  17. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +7 -3
  18. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +2 -2
  19. data/lib/thinking_sphinx/active_record/column_sql_presenter.rb +5 -1
  20. data/lib/thinking_sphinx/active_record/index.rb +4 -4
  21. data/lib/thinking_sphinx/active_record/source_joins.rb +55 -0
  22. data/lib/thinking_sphinx/active_record/sql_builder.rb +3 -18
  23. data/lib/thinking_sphinx/active_record/sql_builder/query.rb +7 -0
  24. data/lib/thinking_sphinx/active_record/sql_source.rb +7 -5
  25. data/lib/thinking_sphinx/active_record/sql_source/template.rb +1 -1
  26. data/lib/thinking_sphinx/callbacks.rb +18 -0
  27. data/lib/thinking_sphinx/configuration.rb +50 -33
  28. data/lib/thinking_sphinx/configuration/duplicate_names.rb +34 -0
  29. data/lib/thinking_sphinx/connection.rb +4 -4
  30. data/lib/thinking_sphinx/controller.rb +6 -4
  31. data/lib/thinking_sphinx/deletion.rb +13 -0
  32. data/lib/thinking_sphinx/errors.rb +8 -0
  33. data/lib/thinking_sphinx/index_set.rb +6 -1
  34. data/lib/thinking_sphinx/indexing_strategies/all_at_once.rb +7 -0
  35. data/lib/thinking_sphinx/indexing_strategies/one_at_a_time.rb +14 -0
  36. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +1 -1
  37. data/lib/thinking_sphinx/middlewares/inquirer.rb +1 -1
  38. data/lib/thinking_sphinx/middlewares/sphinxql.rb +2 -2
  39. data/lib/thinking_sphinx/middlewares/stale_id_checker.rb +1 -1
  40. data/lib/thinking_sphinx/middlewares/stale_id_filter.rb +2 -2
  41. data/lib/thinking_sphinx/rake_interface.rb +14 -5
  42. data/lib/thinking_sphinx/real_time.rb +1 -0
  43. data/lib/thinking_sphinx/real_time/attribute.rb +7 -1
  44. data/lib/thinking_sphinx/real_time/index.rb +2 -0
  45. data/lib/thinking_sphinx/real_time/populator.rb +3 -3
  46. data/lib/thinking_sphinx/real_time/property.rb +1 -5
  47. data/lib/thinking_sphinx/real_time/transcriber.rb +44 -9
  48. data/lib/thinking_sphinx/real_time/translator.rb +36 -0
  49. data/lib/thinking_sphinx/search.rb +10 -0
  50. data/lib/thinking_sphinx/search/context.rb +8 -0
  51. data/lib/thinking_sphinx/search/stale_ids_exception.rb +3 -2
  52. data/lib/thinking_sphinx/subscribers/populator_subscriber.rb +1 -1
  53. data/lib/thinking_sphinx/tasks.rb +3 -1
  54. data/spec/acceptance/remove_deleted_records_spec.rb +18 -0
  55. data/spec/acceptance/searching_with_filters_spec.rb +13 -0
  56. data/spec/internal/app/indices/product_index.rb +1 -0
  57. data/spec/internal/db/schema.rb +1 -0
  58. data/spec/spec_helper.rb +1 -0
  59. data/spec/support/json_column.rb +29 -0
  60. data/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb +10 -0
  61. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +10 -0
  62. data/spec/thinking_sphinx/active_record/column_sql_presenter_spec.rb +37 -0
  63. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +7 -17
  64. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +43 -15
  65. data/spec/thinking_sphinx/errors_spec.rb +7 -0
  66. data/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb +15 -1
  67. data/spec/thinking_sphinx/middlewares/stale_id_checker_spec.rb +1 -0
  68. data/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb +26 -6
  69. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +4 -4
  70. data/spec/thinking_sphinx_spec.rb +2 -1
  71. data/thinking-sphinx.gemspec +1 -1
  72. metadata +12 -3
@@ -15,5 +15,6 @@ require 'thinking_sphinx/real_time/index'
15
15
  require 'thinking_sphinx/real_time/interpreter'
16
16
  require 'thinking_sphinx/real_time/populator'
17
17
  require 'thinking_sphinx/real_time/transcriber'
18
+ require 'thinking_sphinx/real_time/translator'
18
19
 
19
20
  require 'thinking_sphinx/real_time/callbacks/real_time_callbacks'
@@ -8,7 +8,9 @@ class ThinkingSphinx::RealTime::Attribute < ThinkingSphinx::RealTime::Property
8
8
  end
9
9
 
10
10
  def translate(object)
11
- super || default_value
11
+ output = super || default_value
12
+
13
+ json? ? output.to_json : output
12
14
  end
13
15
 
14
16
  private
@@ -16,4 +18,8 @@ class ThinkingSphinx::RealTime::Attribute < ThinkingSphinx::RealTime::Property
16
18
  def default_value
17
19
  type == :string ? '' : 0
18
20
  end
21
+
22
+ def json?
23
+ type == :json
24
+ end
19
25
  end
@@ -69,6 +69,8 @@ class ThinkingSphinx::RealTime::Index < Riddle::Configuration::RealtimeIndex
69
69
  @rt_attr_float
70
70
  when :bigint
71
71
  attribute.multi? ? @rt_attr_multi_64 : @rt_attr_bigint
72
+ when :json
73
+ @rt_attr_json
72
74
  else
73
75
  raise "Unknown attribute type '#{attribute.type}'"
74
76
  end
@@ -12,9 +12,9 @@ class ThinkingSphinx::RealTime::Populator
12
12
 
13
13
  remove_files
14
14
 
15
- scope.find_each do |instance|
16
- transcriber.copy instance
17
- instrument 'populated', :instance => instance
15
+ scope.find_in_batches do |instances|
16
+ transcriber.copy *instances
17
+ instrument 'populated', :instances => instances
18
18
  end
19
19
 
20
20
  controller.rotate
@@ -14,10 +14,6 @@ class ThinkingSphinx::RealTime::Property
14
14
  end
15
15
 
16
16
  def translate(object)
17
- return @column.__name unless @column.__name.is_a?(Symbol)
18
-
19
- base = @column.__stack.inject(object) { |base, node| base.try(node) }
20
- base = base.try(@column.__name)
21
- base.is_a?(String) ? base.gsub("\u0000", '') : base
17
+ ThinkingSphinx::RealTime::Translator.call(object, @column)
22
18
  end
23
19
  end
@@ -3,22 +3,47 @@ class ThinkingSphinx::RealTime::Transcriber
3
3
  @index = index
4
4
  end
5
5
 
6
- def copy(instance)
7
- return unless instance.persisted? && copy?(instance)
6
+ def copy(*instances)
7
+ items = instances.select { |instance|
8
+ instance.persisted? && copy?(instance)
9
+ }
10
+ return unless items.present?
8
11
 
9
- columns, values = ['id'], [index.document_id_for_key(instance.id)]
10
- (index.fields + index.attributes).each do |property|
11
- columns << property.name
12
- values << property.translate(instance)
13
- end
12
+ values = items.collect { |instance|
13
+ TranscribeInstance.call(instance, index, properties)
14
+ }
14
15
 
15
16
  insert = Riddle::Query::Insert.new index.name, columns, values
16
17
  sphinxql = insert.replace!.to_sql
17
-
18
+
18
19
  ThinkingSphinx::Logger.log :query, sphinxql do
19
20
  ThinkingSphinx::Connection.take do |connection|
20
21
  connection.execute sphinxql
21
- end
22
+ end
23
+ end
24
+ end
25
+
26
+ class TranscribeInstance
27
+ def self.call(instance, index, properties)
28
+ new(instance, index, properties).call
29
+ end
30
+
31
+ def initialize(instance, index, properties)
32
+ @instance, @index, @properties = instance, index, properties
33
+ end
34
+
35
+ def call
36
+ properties.each_with_object([document_id]) do |property, instance_values|
37
+ instance_values << property.translate(instance)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :instance, :index, :properties
44
+
45
+ def document_id
46
+ index.document_id_for_key instance.id
22
47
  end
23
48
  end
24
49
 
@@ -26,6 +51,12 @@ class ThinkingSphinx::RealTime::Transcriber
26
51
 
27
52
  attr_reader :index
28
53
 
54
+ def columns
55
+ @columns ||= properties.each_with_object(['id']) do |property, columns|
56
+ columns << property.name
57
+ end
58
+ end
59
+
29
60
  def copy?(instance)
30
61
  index.conditions.empty? || index.conditions.all? { |condition|
31
62
  case condition
@@ -38,4 +69,8 @@ class ThinkingSphinx::RealTime::Transcriber
38
69
  end
39
70
  }
40
71
  end
72
+
73
+ def properties
74
+ @properties ||= index.fields + index.attributes
75
+ end
41
76
  end
@@ -0,0 +1,36 @@
1
+ class ThinkingSphinx::RealTime::Translator
2
+ def self.call(object, column)
3
+ new(object, column).call
4
+ end
5
+
6
+ def initialize(object, column)
7
+ @object, @column = object, column
8
+ end
9
+
10
+ def call
11
+ return name unless name.is_a?(Symbol)
12
+ return result unless result.is_a?(String)
13
+
14
+ result.gsub "\u0000", ''
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :object, :column
20
+
21
+ def name
22
+ @column.__name
23
+ end
24
+
25
+ def owner
26
+ stack.inject(object) { |previous, node| previous.try node }
27
+ end
28
+
29
+ def result
30
+ @result ||= owner.try name
31
+ end
32
+
33
+ def stack
34
+ @column.__stack
35
+ end
36
+ end
@@ -38,6 +38,16 @@ class ThinkingSphinx::Search < Array
38
38
  options[:page].to_i
39
39
  end
40
40
 
41
+ def marshal_dump
42
+ populate
43
+
44
+ [@populated, @query, @options, @context]
45
+ end
46
+
47
+ def marshal_load(array)
48
+ @populated, @query, @options, @context = array
49
+ end
50
+
41
51
  def masks
42
52
  @masks ||= @options[:masks] || DEFAULT_MASKS.clone
43
53
  end
@@ -17,4 +17,12 @@ class ThinkingSphinx::Search::Context
17
17
  def []=(key, value)
18
18
  @memory[key] = value
19
19
  end
20
+
21
+ def marshal_dump
22
+ [@memory.except(:raw, :indices)]
23
+ end
24
+
25
+ def marshal_load(array)
26
+ @memory = array.first
27
+ end
20
28
  end
@@ -1,8 +1,9 @@
1
1
  class ThinkingSphinx::Search::StaleIdsException < StandardError
2
- attr_reader :ids
2
+ attr_reader :ids, :context
3
3
 
4
- def initialize(ids)
4
+ def initialize(ids, context)
5
5
  @ids = ids
6
+ @context = context
6
7
  end
7
8
 
8
9
  def message
@@ -21,7 +21,7 @@ class ThinkingSphinx::Subscribers::PopulatorSubscriber
21
21
  end
22
22
 
23
23
  def populated(event)
24
- print '.'
24
+ print '.' * event.payload[:instances].length
25
25
  end
26
26
 
27
27
  def finish_populating(event)
@@ -39,7 +39,9 @@ namespace :ts do
39
39
 
40
40
  desc 'Start the Sphinx daemon'
41
41
  task :start => :environment do
42
- interface.start
42
+ options = {}
43
+ options[:nodetach] = true if ENV['NODETACH'] == 'true'
44
+ interface.start(options)
43
45
  end
44
46
 
45
47
  desc 'Stop the Sphinx daemon'
@@ -33,6 +33,24 @@ describe 'Hiding deleted records from search results', :live => true do
33
33
  should be_empty
34
34
  end
35
35
 
36
+ it "does not remove real-time results when callbacks are disabled" do
37
+ original = ThinkingSphinx::Configuration.instance.
38
+ settings['real_time_callbacks']
39
+ product = Product.create! :name => 'Shiny'
40
+ Product.search('Shiny', :indices => ['product_core']).to_a.
41
+ should == [product]
42
+
43
+ ThinkingSphinx::Configuration.instance.
44
+ settings['real_time_callbacks'] = false
45
+
46
+ product.destroy
47
+ Product.search_for_ids('Shiny', :indices => ['product_core']).
48
+ should_not be_empty
49
+
50
+ ThinkingSphinx::Configuration.instance.
51
+ settings['real_time_callbacks'] = original
52
+ end
53
+
36
54
  it "deletes STI child classes from parent indices" do
37
55
  duck = Bird.create :name => 'Duck'
38
56
  index
@@ -141,4 +141,17 @@ describe 'Searching with filters', :live => true do
141
141
  products = Product.search :with => {:category_ids => [flat.id]}
142
142
  products.to_a.should == [pancakes]
143
143
  end
144
+
145
+ it 'searches with real-time JSON attributes' do
146
+ pancakes = Product.create :name => 'Pancakes',
147
+ :options => {'lemon' => 1, 'sugar' => 1, :number => 3}
148
+ waffles = Product.create :name => 'Waffles',
149
+ :options => {'chocolate' => 1, 'sugar' => 1, :number => 1}
150
+
151
+ products = Product.search :with => {"options.lemon" => 1}
152
+ products.to_a.should == [pancakes]
153
+
154
+ products = Product.search :with => {"options.sugar" => 1}
155
+ products.to_a.should == [pancakes, waffles]
156
+ end if JSONColumn.call
144
157
  end
@@ -4,6 +4,7 @@ ThinkingSphinx::Index.define :product, :with => :real_time do
4
4
  indexes name, :sortable => true
5
5
 
6
6
  has category_ids, :type => :integer, :multi => true
7
+ has options, :type => :json if JSONColumn.call
7
8
  end
8
9
 
9
10
  if multi_schema.active?
@@ -72,6 +72,7 @@ ActiveRecord::Schema.define do
72
72
 
73
73
  create_table(:products, :force => true) do |t|
74
74
  t.string :name
75
+ t.json :options if ::JSONColumn.call
75
76
  end
76
77
 
77
78
  create_table(:taggings, :force => true) do |t|
@@ -5,6 +5,7 @@ Bundler.require :default, :development
5
5
 
6
6
  root = File.expand_path File.dirname(__FILE__)
7
7
  require "#{root}/support/multi_schema"
8
+ require "#{root}/support/json_column"
8
9
  require 'thinking_sphinx/railtie'
9
10
 
10
11
  Combustion.initialize! :active_record
@@ -0,0 +1,29 @@
1
+ class JSONColumn
2
+ include ActiveRecord::ConnectionAdapters
3
+
4
+ def self.call
5
+ new.call
6
+ end
7
+
8
+ def call
9
+ postgresql? && column?
10
+ end
11
+
12
+ private
13
+
14
+ def column?
15
+ (
16
+ ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQLAdapter) &&
17
+ PostgreSQLAdapter.constants.include?(:TableDefinition) &&
18
+ PostgreSQLAdapter::TableDefinition.instance_methods.include?(:json)
19
+ ) || (
20
+ ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQL) &&
21
+ PostgreSQL.constants.include?(:ColumnMethods) &&
22
+ PostgreSQL::ColumnMethods.instance_methods.include?(:json)
23
+ )
24
+ end
25
+
26
+ def postgresql?
27
+ ENV['DATABASE'] == 'postgresql'
28
+ end
29
+ end
@@ -53,5 +53,15 @@ describe ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks do
53
53
 
54
54
  callbacks.after_destroy
55
55
  end
56
+
57
+ it 'does nothing if callbacks are suspended' do
58
+ ThinkingSphinx::Callbacks.suspend!
59
+
60
+ ThinkingSphinx::Deletion.should_not_receive(:perform)
61
+
62
+ callbacks.after_destroy
63
+
64
+ ThinkingSphinx::Callbacks.resume!
65
+ end
56
66
  end
57
67
  end
@@ -70,5 +70,15 @@ describe ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks do
70
70
 
71
71
  lambda { callbacks.after_update }.should_not raise_error
72
72
  end
73
+
74
+ it 'does nothing if callbacks are suspended' do
75
+ ThinkingSphinx::Callbacks.suspend!
76
+
77
+ connection.should_not_receive(:execute)
78
+
79
+ callbacks.after_update
80
+
81
+ ThinkingSphinx::Callbacks.resume!
82
+ end
73
83
  end
74
84
  end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe ThinkingSphinx::ActiveRecord::ColumnSQLPresenter do
4
+ describe '#with_table' do
5
+ let(:model) { double 'Model' }
6
+ let(:column) { double 'Column', :__name => 'column_name',
7
+ :__stack => [], :string? => false }
8
+ let(:adapter) { double 'Adapter' }
9
+ let(:associations) { double 'Associations' }
10
+ let(:path) { double 'Path',
11
+ :model => double(:column_names => ['column_name']) }
12
+ let(:presenter) { ThinkingSphinx::ActiveRecord::ColumnSQLPresenter.new(
13
+ model, column, adapter, associations
14
+ ) }
15
+
16
+ before do
17
+ stub_const 'Joiner::Path', double(:new => path)
18
+ adapter.stub(:quote) { |arg| "`#{arg}`" }
19
+ end
20
+
21
+ context "when there's no explicit db name" do
22
+ before { associations.stub(:alias_for => 'table_name') }
23
+
24
+ it 'returns quoted table and column names' do
25
+ presenter.with_table.should == '`table_name`.`column_name`'
26
+ end
27
+ end
28
+
29
+ context 'when an eplicit db name is provided' do
30
+ before { associations.stub(:alias_for => 'db_name.table_name') }
31
+
32
+ it 'returns properly quoted table name with column name' do
33
+ presenter.with_table.should == '`db_name`.`table_name`.`column_name`'
34
+ end
35
+ end
36
+ end
37
+ end
@@ -5,7 +5,7 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
5
5
  :fields => [], :attributes => [], :disable_range? => false,
6
6
  :delta_processor => nil, :conditions => [], :groupings => [],
7
7
  :adapter => adapter, :associations => [], :primary_key => :id,
8
- :options => {}) }
8
+ :options => {}, :properties => []) }
9
9
  let(:model) { double('model', :connection => connection,
10
10
  :descends_from_active_record? => true, :column_names => [],
11
11
  :inheritance_column => 'type', :unscoped => relation,
@@ -518,22 +518,6 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
518
518
  end
519
519
  end
520
520
 
521
- describe 'sql_query_post_index' do
522
- let(:processor) { double('processor', :reset_query => 'RESET DELTAS') }
523
-
524
- it "adds a reset delta query if there is a delta processor and this is the core source" do
525
- source.stub :delta_processor => processor, :delta? => false
526
-
527
- builder.sql_query_post_index.should include('RESET DELTAS')
528
- end
529
-
530
- it "adds no reset delta query if there is a delta processor and this is the delta source" do
531
- source.stub :delta_processor => processor, :delta? => true
532
-
533
- builder.sql_query_post_index.should_not include('RESET DELTAS')
534
- end
535
- end
536
-
537
521
  describe 'sql_query_pre' do
538
522
  let(:processor) { double('processor', :reset_query => 'RESET DELTAS') }
539
523
 
@@ -542,6 +526,12 @@ describe ThinkingSphinx::ActiveRecord::SQLBuilder do
542
526
  adapter.stub :utf8_query_pre => ['SET UTF8']
543
527
  end
544
528
 
529
+ it "adds a reset delta query if there is a delta processor and this is the core source" do
530
+ source.stub :delta_processor => processor
531
+
532
+ builder.sql_query_pre.should include('RESET DELTAS')
533
+ end
534
+
545
535
  it "does not add a reset query if there is no delta processor" do
546
536
  builder.sql_query_pre.should_not include('RESET DELTAS')
547
537
  end