thinking-sphinx 4.3.2 → 5.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +152 -0
  3. data/.travis.yml +16 -21
  4. data/Appraisals +11 -20
  5. data/CHANGELOG.markdown +79 -0
  6. data/README.textile +15 -17
  7. data/bin/loadsphinx +30 -7
  8. data/lib/thinking_sphinx.rb +5 -4
  9. data/lib/thinking_sphinx/active_record.rb +1 -0
  10. data/lib/thinking_sphinx/active_record/association_proxy.rb +1 -2
  11. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +1 -1
  12. data/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb +1 -1
  13. data/lib/thinking_sphinx/active_record/base.rb +17 -6
  14. data/lib/thinking_sphinx/active_record/callbacks/association_delta_callbacks.rb +21 -0
  15. data/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +1 -1
  16. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +3 -2
  17. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +1 -1
  18. data/lib/thinking_sphinx/active_record/interpreter.rb +4 -4
  19. data/lib/thinking_sphinx/active_record/sql_source.rb +12 -0
  20. data/lib/thinking_sphinx/active_record/sql_source/template.rb +2 -2
  21. data/lib/thinking_sphinx/callbacks.rb +9 -0
  22. data/lib/thinking_sphinx/callbacks/appender.rb +59 -0
  23. data/lib/thinking_sphinx/commands/index_real_time.rb +1 -3
  24. data/lib/thinking_sphinx/connection.rb +4 -0
  25. data/lib/thinking_sphinx/connection/client.rb +6 -1
  26. data/lib/thinking_sphinx/core/index.rb +5 -2
  27. data/lib/thinking_sphinx/deletion.rb +18 -17
  28. data/lib/thinking_sphinx/errors.rb +1 -1
  29. data/lib/thinking_sphinx/index_set.rb +7 -3
  30. data/lib/thinking_sphinx/middlewares/sphinxql.rb +1 -1
  31. data/lib/thinking_sphinx/railtie.rb +14 -1
  32. data/lib/thinking_sphinx/real_time.rb +17 -0
  33. data/lib/thinking_sphinx/real_time/index.rb +5 -3
  34. data/lib/thinking_sphinx/real_time/index/template.rb +12 -0
  35. data/lib/thinking_sphinx/real_time/interpreter.rb +8 -6
  36. data/lib/thinking_sphinx/real_time/populator.rb +5 -2
  37. data/lib/thinking_sphinx/real_time/processor.rb +36 -0
  38. data/lib/thinking_sphinx/real_time/transcriber.rb +41 -19
  39. data/lib/thinking_sphinx/real_time/translator.rb +1 -0
  40. data/lib/thinking_sphinx/settings.rb +13 -9
  41. data/lib/thinking_sphinx/subscribers/populator_subscriber.rb +0 -4
  42. data/spec/acceptance/big_integers_spec.rb +1 -1
  43. data/spec/acceptance/geosearching_spec.rb +13 -3
  44. data/spec/acceptance/merging_spec.rb +1 -1
  45. data/spec/acceptance/real_time_updates_spec.rb +2 -2
  46. data/spec/acceptance/sql_deltas_spec.rb +15 -3
  47. data/spec/acceptance/support/sphinx_helpers.rb +4 -4
  48. data/spec/acceptance/suspended_deltas_spec.rb +3 -3
  49. data/spec/internal/app/indices/article_index.rb +0 -1
  50. data/spec/internal/app/indices/colour_index.rb +7 -0
  51. data/spec/internal/app/models/admin/person.rb +3 -1
  52. data/spec/internal/app/models/album.rb +3 -1
  53. data/spec/internal/app/models/animal.rb +1 -0
  54. data/spec/internal/app/models/article.rb +2 -0
  55. data/spec/internal/app/models/bird.rb +1 -0
  56. data/spec/internal/app/models/book.rb +2 -0
  57. data/spec/internal/app/models/car.rb +1 -1
  58. data/spec/internal/app/models/city.rb +2 -0
  59. data/spec/internal/app/models/colour.rb +2 -0
  60. data/spec/internal/app/models/product.rb +1 -1
  61. data/spec/internal/app/models/tee.rb +5 -0
  62. data/spec/internal/app/models/user.rb +2 -0
  63. data/spec/internal/config/database.yml +6 -1
  64. data/spec/internal/db/schema.rb +1 -0
  65. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +2 -1
  66. data/spec/thinking_sphinx/active_record/index_spec.rb +0 -12
  67. data/spec/thinking_sphinx/active_record/interpreter_spec.rb +15 -14
  68. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +41 -3
  69. data/spec/thinking_sphinx/connection/mri_spec.rb +49 -0
  70. data/spec/thinking_sphinx/deletion_spec.rb +5 -4
  71. data/spec/thinking_sphinx/index_set_spec.rb +28 -12
  72. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +2 -1
  73. data/spec/thinking_sphinx/real_time/index_spec.rb +51 -13
  74. data/spec/thinking_sphinx/real_time/interpreter_spec.rb +14 -14
  75. data/spec/thinking_sphinx/real_time/transcriber_spec.rb +9 -1
  76. data/thinking-sphinx.gemspec +3 -5
  77. metadata +17 -11
  78. data/spec/acceptance/connection_spec.rb +0 -25
@@ -133,7 +133,7 @@ class ThinkingSphinx::Middlewares::SphinxQL <
133
133
 
134
134
  def indices_match_classes?
135
135
  indices.collect(&:reference).uniq.sort == classes.collect { |klass|
136
- ThinkingSphinx::IndexSet.reference_name(klass)
136
+ configuration.index_set_class.reference_name(klass)
137
137
  }.sort
138
138
  end
139
139
 
@@ -7,7 +7,20 @@ class ThinkingSphinx::Railtie < Rails::Railtie
7
7
 
8
8
  initializer 'thinking_sphinx.initialisation' do
9
9
  ActiveSupport.on_load(:active_record) do
10
- ActiveRecord::Base.send :include, ThinkingSphinx::ActiveRecord::Base
10
+ require 'thinking_sphinx/active_record'
11
+ ActiveRecord::Base.include ThinkingSphinx::ActiveRecord::Base
12
+ end
13
+
14
+ if ActiveSupport::VERSION::MAJOR > 5
15
+ if Rails.application.config.autoloader == :zeitwerk
16
+ ActiveSupport::Dependencies.autoload_paths.delete(
17
+ Rails.root.join("app", "indices").to_s
18
+ )
19
+ end
20
+
21
+ Rails.application.config.eager_load_paths -=
22
+ ThinkingSphinx::Configuration.instance.index_paths
23
+ Rails.application.config.eager_load_paths.freeze
11
24
  end
12
25
  end
13
26
 
@@ -8,6 +8,22 @@ module ThinkingSphinx::RealTime
8
8
  def self.callback_for(reference, path = [], &block)
9
9
  Callbacks::RealTimeCallbacks.new reference.to_sym, path, &block
10
10
  end
11
+
12
+ def self.populator
13
+ @populator ||= ThinkingSphinx::RealTime::Populator
14
+ end
15
+
16
+ def self.populator=(value)
17
+ @populator = value
18
+ end
19
+
20
+ def self.processor
21
+ @processor ||= ThinkingSphinx::RealTime::Processor
22
+ end
23
+
24
+ def self.processor=(value)
25
+ @processor = value
26
+ end
11
27
  end
12
28
 
13
29
  require 'thinking_sphinx/real_time/property'
@@ -16,6 +32,7 @@ require 'thinking_sphinx/real_time/field'
16
32
  require 'thinking_sphinx/real_time/index'
17
33
  require 'thinking_sphinx/real_time/interpreter'
18
34
  require 'thinking_sphinx/real_time/populator'
35
+ require 'thinking_sphinx/real_time/processor'
19
36
  require 'thinking_sphinx/real_time/transcribe_instance'
20
37
  require 'thinking_sphinx/real_time/transcriber'
21
38
  require 'thinking_sphinx/real_time/translator'
@@ -16,10 +16,14 @@ class ThinkingSphinx::RealTime::Index < Riddle::Configuration::RealtimeIndex
16
16
  end
17
17
 
18
18
  def add_attribute(attribute)
19
+ @attributes.delete_if { |existing| existing.name == attribute.name }
20
+
19
21
  @attributes << attribute
20
22
  end
21
23
 
22
24
  def add_field(field)
25
+ @fields.delete_if { |existing| existing.name == field.name }
26
+
23
27
  @fields << field
24
28
  end
25
29
 
@@ -61,12 +65,10 @@ class ThinkingSphinx::RealTime::Index < Riddle::Configuration::RealtimeIndex
61
65
 
62
66
  def collection_for(attribute)
63
67
  case attribute.type
64
- when :integer, :boolean
68
+ when :integer, :boolean, :timestamp
65
69
  attribute.multi? ? @rt_attr_multi : @rt_attr_uint
66
70
  when :string
67
71
  @rt_attr_string
68
- when :timestamp
69
- @rt_attr_timestamp
70
72
  when :float
71
73
  @rt_attr_float
72
74
  when :bigint
@@ -13,6 +13,10 @@ class ThinkingSphinx::RealTime::Index::Template
13
13
  add_attribute primary_key, :sphinx_internal_id, :bigint
14
14
  add_attribute class_column, :sphinx_internal_class, :string, :facet => true
15
15
  add_attribute 0, :sphinx_deleted, :integer
16
+
17
+ if tidying?
18
+ add_attribute -> (_) { Time.current.to_i }, :sphinx_updated_at, :timestamp
19
+ end
16
20
  end
17
21
 
18
22
  private
@@ -34,7 +38,15 @@ class ThinkingSphinx::RealTime::Index::Template
34
38
  [:class, :name]
35
39
  end
36
40
 
41
+ def config
42
+ ThinkingSphinx::Configuration.instance
43
+ end
44
+
37
45
  def primary_key
38
46
  index.primary_key.to_sym
39
47
  end
48
+
49
+ def tidying?
50
+ config.settings["real_time_tidy"]
51
+ end
40
52
  end
@@ -5,16 +5,18 @@ class ThinkingSphinx::RealTime::Interpreter <
5
5
 
6
6
  def has(*columns)
7
7
  options = columns.extract_options!
8
- @index.attributes += columns.collect { |column|
8
+
9
+ columns.collect { |column|
9
10
  ::ThinkingSphinx::RealTime::Attribute.new column, options
10
- }
11
+ }.each { |attribute| @index.add_attribute attribute }
11
12
  end
12
13
 
13
14
  def indexes(*columns)
14
15
  options = columns.extract_options!
15
- @index.fields += columns.collect { |column|
16
+
17
+ columns.collect { |column|
16
18
  ::ThinkingSphinx::RealTime::Field.new column, options
17
- }
19
+ }.each { |field| @index.add_field field }
18
20
 
19
21
  append_sortable_attributes columns, options if options[:sortable]
20
22
  end
@@ -39,7 +41,7 @@ class ThinkingSphinx::RealTime::Interpreter <
39
41
  def append_sortable_attributes(columns, options)
40
42
  options = options.except(:sortable).merge(:type => :string)
41
43
 
42
- @index.attributes += columns.collect { |column|
44
+ columns.collect { |column|
43
45
  aliased_name = options[:as]
44
46
  aliased_name ||= column.__name.to_sym if column.respond_to?(:__name)
45
47
  aliased_name ||= column
@@ -47,6 +49,6 @@ class ThinkingSphinx::RealTime::Interpreter <
47
49
  options[:as] = "#{aliased_name}_sort".to_sym
48
50
 
49
51
  ::ThinkingSphinx::RealTime::Attribute.new column, options
50
- }
52
+ }.each { |attribute| @index.add_attribute attribute }
51
53
  end
52
54
  end
@@ -7,9 +7,10 @@ class ThinkingSphinx::RealTime::Populator
7
7
 
8
8
  def initialize(index)
9
9
  @index = index
10
+ @started_at = Time.current
10
11
  end
11
12
 
12
- def populate(&block)
13
+ def populate
13
14
  instrument 'start_populating'
14
15
 
15
16
  scope.find_in_batches(:batch_size => batch_size) do |instances|
@@ -17,12 +18,14 @@ class ThinkingSphinx::RealTime::Populator
17
18
  instrument 'populated', :instances => instances
18
19
  end
19
20
 
21
+ transcriber.clear_before(started_at) if configuration.settings["real_time_tidy"]
22
+
20
23
  instrument 'finish_populating'
21
24
  end
22
25
 
23
26
  private
24
27
 
25
- attr_reader :index
28
+ attr_reader :index, :started_at
26
29
 
27
30
  delegate :controller, :batch_size, :to => :configuration
28
31
  delegate :scope, :to => :index
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ThinkingSphinx::RealTime::Processor
4
+ def self.call(indices, &block)
5
+ new(indices).call(&block)
6
+ end
7
+
8
+ def initialize(indices)
9
+ @indices = indices
10
+ end
11
+
12
+ def call(&block)
13
+ subscribe_to_progress
14
+
15
+ indices.each do |index|
16
+ ThinkingSphinx::RealTime.populator.populate index
17
+
18
+ block.call
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :indices
25
+
26
+ def command
27
+ ThinkingSphinx::Commander.call(
28
+ command, configuration, options, stream
29
+ )
30
+ end
31
+
32
+ def subscribe_to_progress
33
+ ThinkingSphinx::Subscribers::PopulatorSubscriber.
34
+ attach_to 'thinking_sphinx.real_time'
35
+ end
36
+ end
@@ -5,31 +5,20 @@ class ThinkingSphinx::RealTime::Transcriber
5
5
  @index = index
6
6
  end
7
7
 
8
+ def clear_before(time)
9
+ execute <<~SQL.strip
10
+ DELETE FROM #{@index.name} WHERE sphinx_updated_at < #{time.to_i}
11
+ SQL
12
+ end
13
+
8
14
  def copy(*instances)
9
15
  items = instances.select { |instance|
10
16
  instance.persisted? && copy?(instance)
11
17
  }
12
18
  return unless items.present?
13
19
 
14
- values = []
15
- items.each do |instance|
16
- begin
17
- values << ThinkingSphinx::RealTime::TranscribeInstance.call(
18
- instance, index, properties
19
- )
20
- rescue ThinkingSphinx::TranscriptionError => error
21
- instrument 'error', :error => error
22
- end
23
- end
24
-
25
- insert = Riddle::Query::Insert.new index.name, columns, values
26
- sphinxql = insert.replace!.to_sql
27
-
28
- ThinkingSphinx::Logger.log :query, sphinxql do
29
- ThinkingSphinx::Connection.take do |connection|
30
- connection.execute sphinxql
31
- end
32
- end
20
+ delete_existing items
21
+ insert_replacements items
33
22
  end
34
23
 
35
24
  private
@@ -55,6 +44,27 @@ class ThinkingSphinx::RealTime::Transcriber
55
44
  }
56
45
  end
57
46
 
47
+ def delete_existing(instances)
48
+ ids = instances.collect(&index.primary_key.to_sym)
49
+
50
+ execute <<~SQL.strip
51
+ DELETE FROM #{@index.name} WHERE sphinx_internal_id IN (#{ids.join(', ')})
52
+ SQL
53
+ end
54
+
55
+ def execute(sphinxql)
56
+ ThinkingSphinx::Logger.log :query, sphinxql do
57
+ ThinkingSphinx::Connection.take do |connection|
58
+ connection.execute sphinxql
59
+ end
60
+ end
61
+ end
62
+
63
+ def insert_replacements(instances)
64
+ insert = Riddle::Query::Insert.new index.name, columns, values(instances)
65
+ execute insert.replace!.to_sql
66
+ end
67
+
58
68
  def instrument(message, options = {})
59
69
  ActiveSupport::Notifications.instrument(
60
70
  "#{message}.thinking_sphinx.real_time", options.merge(:index => index)
@@ -64,4 +74,16 @@ class ThinkingSphinx::RealTime::Transcriber
64
74
  def properties
65
75
  @properties ||= index.fields + index.attributes
66
76
  end
77
+
78
+ def values(instances)
79
+ instances.each_with_object([]) do |instance, array|
80
+ begin
81
+ array << ThinkingSphinx::RealTime::TranscribeInstance.call(
82
+ instance, index, properties
83
+ )
84
+ rescue ThinkingSphinx::TranscriptionError => error
85
+ instrument 'error', :error => error
86
+ end
87
+ end
88
+ end
67
89
  end
@@ -10,6 +10,7 @@ class ThinkingSphinx::RealTime::Translator
10
10
  end
11
11
 
12
12
  def call
13
+ return name.call(object) if name.is_a?(Proc)
13
14
  return name unless name.is_a?(Symbol)
14
15
  return result unless result.is_a?(String)
15
16
 
@@ -11,14 +11,16 @@ class ThinkingSphinx::Settings
11
11
  lemmatizer_base mysql_ssl_cert mysql_ssl_key mysql_ssl_ca
12
12
  ].freeze
13
13
  DEFAULTS = {
14
- "configuration_file" => "config/ENVIRONMENT.sphinx.conf",
15
- "indices_location" => "db/sphinx/ENVIRONMENT",
16
- "pid_file" => "log/ENVIRONMENT.sphinx.pid",
17
- "log" => "log/ENVIRONMENT.searchd.log",
18
- "query_log" => "log/ENVIRONMENT.searchd.query.log",
19
- "binlog_path" => "tmp/binlog/ENVIRONMENT",
20
- "workers" => "threads",
21
- "mysql_encoding" => "utf8"
14
+ "configuration_file" => "config/ENVIRONMENT.sphinx.conf",
15
+ "indices_location" => "db/sphinx/ENVIRONMENT",
16
+ "pid_file" => "log/ENVIRONMENT.sphinx.pid",
17
+ "log" => "log/ENVIRONMENT.searchd.log",
18
+ "query_log" => "log/ENVIRONMENT.searchd.query.log",
19
+ "binlog_path" => "tmp/binlog/ENVIRONMENT",
20
+ "workers" => "threads",
21
+ "mysql_encoding" => "utf8",
22
+ "maximum_statement_length" => (2 ** 23) - 5,
23
+ "real_time_tidy" => false
22
24
  }.freeze
23
25
 
24
26
  def self.call(configuration)
@@ -62,7 +64,9 @@ class ThinkingSphinx::Settings
62
64
 
63
65
  def defaults
64
66
  DEFAULTS.inject({}) do |hash, (key, value)|
65
- value = value.gsub("ENVIRONMENT", framework.environment)
67
+ if value.is_a?(String)
68
+ value = value.gsub("ENVIRONMENT", framework.environment)
69
+ end
66
70
 
67
71
  if FILE_KEYS.include?(key)
68
72
  hash[key] = absolute value
@@ -46,7 +46,3 @@ Error transcribing #{instance.class} #{instance.id}:
46
46
  delegate :output, :to => ThinkingSphinx
47
47
  delegate :puts, :print, :to => :output
48
48
  end
49
-
50
- ThinkingSphinx::Subscribers::PopulatorSubscriber.attach_to(
51
- 'thinking_sphinx.real_time'
52
- )
@@ -52,7 +52,7 @@ describe '64 bit document ids', :live => true do
52
52
  context 'with Real-Time' do
53
53
  it 'handles large 32 bit integers with an offset multiplier' do
54
54
  product = Product.create! :name => "Widget"
55
- product.update_attributes :id => 980190962
55
+ product.update :id => 980190962
56
56
  expect(
57
57
  Product.search('widget', :indices => ['product_core']).to_a
58
58
  ).to eq([product])
@@ -38,10 +38,20 @@ describe 'Searching by latitude and longitude', :live => true do
38
38
  expected = {:mysql => 250326.906250, :postgresql => 250331.234375}
39
39
  end
40
40
 
41
- if ActiveRecord::Base.configurations['test']['adapter'][/postgres/]
42
- expect(cities.first.geodist).to eq(expected[:postgresql])
41
+ adapter = nil
42
+
43
+ if ActiveRecord::VERSION::STRING.to_f > 6.0
44
+ adapter = ActiveRecord::Base.configurations.configs_for.first.adapter
45
+ elsif ActiveRecord::VERSION::STRING.to_f > 5.2
46
+ adapter = ActiveRecord::Base.configurations.configs_for.first.config["adapter"]
47
+ else
48
+ adapter = ActiveRecord::Base.configurations['test']['adapter']
49
+ end
50
+
51
+ if adapter[/postgres/]
52
+ expect(cities.first.geodist).to be_within(0.01).of(expected[:postgresql])
43
53
  else # mysql
44
- expect(cities.first.geodist).to eq(expected[:mysql])
54
+ expect(cities.first.geodist).to be_within(0.01).of(expected[:mysql])
45
55
  end
46
56
  end
47
57
 
@@ -34,7 +34,7 @@ describe "Merging deltas", :live => true do
34
34
  Book.search("Space", :indices => ["book_core"]).to_a
35
35
  ).to eq([race])
36
36
 
37
- race.reload.update_attributes :title => "The Hate Race"
37
+ race.reload.update :title => "The Hate Race"
38
38
  sleep 0.25
39
39
  expect(
40
40
  Book.search("Race", :indices => ["book_delta"]).to_a
@@ -11,7 +11,7 @@ describe 'Updates to records in real-time indices', :live => true do
11
11
 
12
12
  it "handles attributes for sortable fields accordingly" do
13
13
  product = Product.create! :name => 'Red Fish'
14
- product.update_attributes :name => 'Blue Fish'
14
+ product.update :name => 'Blue Fish'
15
15
 
16
16
  expect(Product.search('blue fish', :indices => ['product_core']).to_a).
17
17
  to eq([product])
@@ -22,7 +22,7 @@ describe 'Updates to records in real-time indices', :live => true do
22
22
 
23
23
  expect(Admin::Person.search('Death').to_a).to eq([person])
24
24
 
25
- person.update_attributes :name => 'Mort'
25
+ person.update :name => 'Mort'
26
26
 
27
27
  expect(Admin::Person.search('Death').to_a).to be_empty
28
28
  expect(Admin::Person.search('Mort').to_a).to eq([person])
@@ -25,7 +25,7 @@ describe 'SQL delta indexing', :live => true do
25
25
 
26
26
  expect(Book.search('Harry').to_a).to eq([book])
27
27
 
28
- book.reload.update_attributes(:author => 'Terry Pratchett')
28
+ book.reload.update(:author => 'Terry Pratchett')
29
29
  sleep 0.25
30
30
 
31
31
  expect(Book.search('Terry').to_a).to eq([book])
@@ -37,7 +37,7 @@ describe 'SQL delta indexing', :live => true do
37
37
 
38
38
  expect(Book.search('Harry').to_a).to eq([book])
39
39
 
40
- book.reload.update_attributes(:author => 'Terry Pratchett')
40
+ book.reload.update(:author => 'Terry Pratchett')
41
41
  sleep 0.25
42
42
 
43
43
  expect(Book.search('Harry')).to be_empty
@@ -49,7 +49,7 @@ describe 'SQL delta indexing', :live => true do
49
49
 
50
50
  expect(Album.search('Whitloms').to_a).to eq([album])
51
51
 
52
- album.reload.update_attributes(:artist => 'The Whitlams')
52
+ album.reload.update(:artist => 'The Whitlams')
53
53
  sleep 0.25
54
54
 
55
55
  expect(Book.search('Whitloms')).to be_empty
@@ -63,4 +63,16 @@ describe 'SQL delta indexing', :live => true do
63
63
 
64
64
  expect(Book.search('Gaiman').to_a).to eq([book])
65
65
  end
66
+
67
+ it "updates associated models" do
68
+ colour = Colour.create(:name => 'green')
69
+ sleep 0.25
70
+
71
+ expect(Colour.search('green').to_a).to eq([colour])
72
+
73
+ tee = colour.tees.create
74
+ sleep 0.25
75
+
76
+ expect(Colour.search(:with => {:tee_ids => tee.id}).to_a).to eq([colour])
77
+ end
66
78
  end