thinking-sphinx 3.1.4 → 3.2.0

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 (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