thinking-sphinx 4.4.1 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +218 -0
  3. data/.travis.yml +12 -21
  4. data/Appraisals +16 -19
  5. data/CHANGELOG.markdown +93 -0
  6. data/README.textile +17 -17
  7. data/bin/loadsphinx +22 -4
  8. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +1 -1
  9. data/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb +1 -1
  10. data/lib/thinking_sphinx/active_record/base.rb +2 -6
  11. data/lib/thinking_sphinx/active_record/callbacks/association_delta_callbacks.rb +21 -0
  12. data/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +6 -2
  13. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +3 -2
  14. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +1 -1
  15. data/lib/thinking_sphinx/active_record/interpreter.rb +4 -4
  16. data/lib/thinking_sphinx/active_record/sql_source/template.rb +2 -2
  17. data/lib/thinking_sphinx/active_record/sql_source.rb +12 -0
  18. data/lib/thinking_sphinx/active_record.rb +3 -0
  19. data/lib/thinking_sphinx/callbacks/appender.rb +63 -0
  20. data/lib/thinking_sphinx/callbacks.rb +9 -0
  21. data/lib/thinking_sphinx/connection/client.rb +6 -1
  22. data/lib/thinking_sphinx/connection.rb +4 -0
  23. data/lib/thinking_sphinx/core/index.rb +1 -2
  24. data/lib/thinking_sphinx/deletion.rb +18 -17
  25. data/lib/thinking_sphinx/errors.rb +1 -1
  26. data/lib/thinking_sphinx/index_set.rb +7 -3
  27. data/lib/thinking_sphinx/middlewares/sphinxql.rb +1 -1
  28. data/lib/thinking_sphinx/railtie.rb +20 -7
  29. data/lib/thinking_sphinx/real_time/index/template.rb +12 -0
  30. data/lib/thinking_sphinx/real_time/index.rb +5 -3
  31. data/lib/thinking_sphinx/real_time/interpreter.rb +8 -6
  32. data/lib/thinking_sphinx/real_time/populator.rb +4 -1
  33. data/lib/thinking_sphinx/real_time/transcriber.rb +41 -19
  34. data/lib/thinking_sphinx/real_time/translator.rb +1 -0
  35. data/lib/thinking_sphinx/search/stale_ids_exception.rb +2 -1
  36. data/lib/thinking_sphinx/search.rb +1 -1
  37. data/lib/thinking_sphinx/settings.rb +13 -9
  38. data/lib/thinking_sphinx/sinatra.rb +1 -1
  39. data/lib/thinking_sphinx/test.rb +1 -1
  40. data/lib/thinking_sphinx.rb +0 -3
  41. data/spec/acceptance/attribute_access_spec.rb +10 -2
  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/paginating_search_results_spec.rb +18 -2
  46. data/spec/acceptance/real_time_updates_spec.rb +2 -2
  47. data/spec/acceptance/searching_with_filters_spec.rb +3 -3
  48. data/spec/acceptance/sql_deltas_spec.rb +16 -4
  49. data/spec/acceptance/support/sphinx_controller.rb +6 -4
  50. data/spec/acceptance/support/sphinx_helpers.rb +4 -4
  51. data/spec/acceptance/suspended_deltas_spec.rb +3 -3
  52. data/spec/internal/app/indices/article_index.rb +0 -1
  53. data/spec/internal/app/indices/colour_index.rb +7 -0
  54. data/spec/internal/app/models/admin/person.rb +3 -1
  55. data/spec/internal/app/models/album.rb +3 -1
  56. data/spec/internal/app/models/animal.rb +1 -0
  57. data/spec/internal/app/models/article.rb +2 -0
  58. data/spec/internal/app/models/bird.rb +1 -0
  59. data/spec/internal/app/models/book.rb +2 -0
  60. data/spec/internal/app/models/car.rb +1 -1
  61. data/spec/internal/app/models/city.rb +2 -0
  62. data/spec/internal/app/models/colour.rb +2 -0
  63. data/spec/internal/app/models/product.rb +1 -1
  64. data/spec/internal/app/models/tee.rb +5 -0
  65. data/spec/internal/app/models/user.rb +2 -0
  66. data/spec/internal/config/database.yml +6 -1
  67. data/spec/internal/db/schema.rb +1 -0
  68. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +2 -1
  69. data/spec/thinking_sphinx/active_record/index_spec.rb +3 -13
  70. data/spec/thinking_sphinx/active_record/interpreter_spec.rb +15 -14
  71. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +41 -3
  72. data/spec/thinking_sphinx/connection/mri_spec.rb +49 -0
  73. data/spec/thinking_sphinx/deletion_spec.rb +5 -4
  74. data/spec/thinking_sphinx/index_set_spec.rb +28 -12
  75. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +2 -1
  76. data/spec/thinking_sphinx/real_time/index_spec.rb +51 -13
  77. data/spec/thinking_sphinx/real_time/interpreter_spec.rb +14 -14
  78. data/spec/thinking_sphinx/real_time/transcriber_spec.rb +9 -1
  79. data/thinking-sphinx.gemspec +3 -3
  80. metadata +17 -12
  81. data/spec/acceptance/connection_spec.rb +0 -25
@@ -3,7 +3,11 @@
3
3
  class ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks <
4
4
  ThinkingSphinx::Callbacks
5
5
 
6
- callbacks :after_destroy, :after_rollback
6
+ callbacks :after_commit, :after_destroy, :after_rollback
7
+
8
+ def after_commit
9
+ delete_from_sphinx
10
+ end
7
11
 
8
12
  def after_destroy
9
13
  delete_from_sphinx
@@ -27,7 +31,7 @@ class ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks <
27
31
 
28
32
  def indices
29
33
  ThinkingSphinx::Configuration.instance.index_set_class.new(
30
- :classes => [instance.class]
34
+ :instances => [instance], :classes => [instance.class]
31
35
  ).to_a
32
36
  end
33
37
  end
@@ -43,8 +43,9 @@ class ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks <
43
43
  end
44
44
 
45
45
  def indices
46
- @indices ||= config.index_set_class.new(:classes => [instance.class]).
47
- select { |index| index.type == "plain" }
46
+ @indices ||= config.index_set_class.new(
47
+ :instances => [instance], :classes => [instance.class]
48
+ ).select { |index| index.type == "plain" }
48
49
  end
49
50
 
50
51
  def new_or_changed?
@@ -47,7 +47,7 @@ class ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks <
47
47
  end
48
48
 
49
49
  def reference
50
- ThinkingSphinx::IndexSet.reference_name(instance.class)
50
+ configuration.index_set_class.reference_name(instance.class)
51
51
  end
52
52
 
53
53
  def update(index)
@@ -13,15 +13,15 @@ class ThinkingSphinx::ActiveRecord::Interpreter <
13
13
  end
14
14
 
15
15
  def has(*columns)
16
- __source.attributes += build_properties(
16
+ build_properties(
17
17
  ::ThinkingSphinx::ActiveRecord::Attribute, columns
18
- )
18
+ ).each { |attribute| __source.add_attribute attribute }
19
19
  end
20
20
 
21
21
  def indexes(*columns)
22
- __source.fields += build_properties(
22
+ build_properties(
23
23
  ::ThinkingSphinx::ActiveRecord::Field, columns
24
- )
24
+ ).each { |field| __source.add_field field }
25
25
  end
26
26
 
27
27
  def join(*columns)
@@ -18,14 +18,14 @@ class ThinkingSphinx::ActiveRecord::SQLSource::Template
18
18
  private
19
19
 
20
20
  def add_attribute(column, name, type, options = {})
21
- source.attributes << ThinkingSphinx::ActiveRecord::Attribute.new(
21
+ source.add_attribute ThinkingSphinx::ActiveRecord::Attribute.new(
22
22
  source.model, ThinkingSphinx::ActiveRecord::Column.new(column),
23
23
  options.merge(:as => name, :type => type)
24
24
  )
25
25
  end
26
26
 
27
27
  def add_field(column, name, options = {})
28
- source.fields << ThinkingSphinx::ActiveRecord::Field.new(
28
+ source.add_field ThinkingSphinx::ActiveRecord::Field.new(
29
29
  source.model, ThinkingSphinx::ActiveRecord::Column.new(column),
30
30
  options.merge(:as => name)
31
31
  )
@@ -39,6 +39,18 @@ module ThinkingSphinx
39
39
  @adapter ||= DatabaseAdapters.adapter_for(@model)
40
40
  end
41
41
 
42
+ def add_attribute(attribute)
43
+ attributes.delete_if { |existing| existing.name == attribute.name }
44
+
45
+ attributes << attribute
46
+ end
47
+
48
+ def add_field(field)
49
+ fields.delete_if { |existing| existing.name == field.name }
50
+
51
+ fields << field
52
+ end
53
+
42
54
  def delta_processor
43
55
  options[:delta_processor].try(:new, adapter, @options[:delta_options] || {})
44
56
  end
@@ -29,6 +29,7 @@ require 'thinking_sphinx/active_record/source_joins'
29
29
  require 'thinking_sphinx/active_record/sql_builder'
30
30
  require 'thinking_sphinx/active_record/sql_source'
31
31
 
32
+ require 'thinking_sphinx/active_record/callbacks/association_delta_callbacks'
32
33
  require 'thinking_sphinx/active_record/callbacks/delete_callbacks'
33
34
  require 'thinking_sphinx/active_record/callbacks/delta_callbacks'
34
35
  require 'thinking_sphinx/active_record/callbacks/update_callbacks'
@@ -39,3 +40,5 @@ require 'thinking_sphinx/active_record/depolymorph/conditions_reflection'
39
40
  require 'thinking_sphinx/active_record/depolymorph/overridden_reflection'
40
41
  require 'thinking_sphinx/active_record/depolymorph/scoped_reflection'
41
42
  require 'thinking_sphinx/active_record/filter_reflection'
43
+
44
+ ActiveRecord::Base.include ThinkingSphinx::ActiveRecord::Base
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ThinkingSphinx::Callbacks::Appender
4
+ def self.call(model, reference, options, &block)
5
+ new(model, reference, options, &block).call
6
+ end
7
+
8
+ def initialize(model, reference, options, &block)
9
+ @model = model
10
+ @reference = reference
11
+ @options = options
12
+ @block = block
13
+ end
14
+
15
+ def call
16
+ add_core_callbacks
17
+ add_delta_callbacks if behaviours.include?(:deltas)
18
+ add_real_time_callbacks if behaviours.include?(:real_time)
19
+ add_update_callbacks if behaviours.include?(:updates)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :model, :reference, :options, :block
25
+
26
+ def add_core_callbacks
27
+ model.after_commit(
28
+ ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks,
29
+ on: :destroy
30
+ )
31
+ end
32
+
33
+ def add_delta_callbacks
34
+ if path.empty?
35
+ model.before_save ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks
36
+ model.after_commit ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks
37
+ else
38
+ model.after_commit(
39
+ ThinkingSphinx::ActiveRecord::Callbacks::AssociationDeltaCallbacks
40
+ .new(path)
41
+ )
42
+ end
43
+ end
44
+
45
+ def add_real_time_callbacks
46
+ model.after_commit(
47
+ ThinkingSphinx::RealTime.callback_for(reference, path, &block),
48
+ on: [:create, :update]
49
+ )
50
+ end
51
+
52
+ def add_update_callbacks
53
+ model.after_update ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks
54
+ end
55
+
56
+ def behaviours
57
+ options[:behaviours] || []
58
+ end
59
+
60
+ def path
61
+ options[:path] || []
62
+ end
63
+ end
@@ -3,6 +3,13 @@
3
3
  class ThinkingSphinx::Callbacks
4
4
  attr_reader :instance
5
5
 
6
+ def self.append(model, reference = nil, options, &block)
7
+ reference ||= ThinkingSphinx::Configuration.instance.index_set_class.
8
+ reference_name(model)
9
+
10
+ ThinkingSphinx::Callbacks::Appender.call(model, reference, options, &block)
11
+ end
12
+
6
13
  def self.callbacks(*methods)
7
14
  mod = Module.new
8
15
  methods.each do |method|
@@ -33,3 +40,5 @@ class ThinkingSphinx::Callbacks
33
40
  @instance = instance
34
41
  end
35
42
  end
43
+
44
+ require "thinking_sphinx/callbacks/appender"
@@ -39,7 +39,7 @@ class ThinkingSphinx::Connection::Client
39
39
  private
40
40
 
41
41
  def check(statements)
42
- if statements.length > ThinkingSphinx::MAXIMUM_STATEMENT_LENGTH
42
+ if statements.length > maximum_statement_length
43
43
  exception = ThinkingSphinx::QueryLengthError.new
44
44
  exception.statement = statements
45
45
  raise exception
@@ -56,6 +56,11 @@ class ThinkingSphinx::Connection::Client
56
56
  @client = nil
57
57
  end
58
58
 
59
+ def maximum_statement_length
60
+ @maximum_statement_length ||= ThinkingSphinx::Configuration.instance.
61
+ settings['maximum_statement_length']
62
+ end
63
+
59
64
  def perform(statements)
60
65
  results_for statements
61
66
  rescue => error
@@ -16,6 +16,10 @@ module ThinkingSphinx::Connection
16
16
  connection_class.new options
17
17
  end
18
18
 
19
+ def self.clear
20
+ @pool = nil
21
+ end
22
+
19
23
  def self.connection_class
20
24
  return ThinkingSphinx::Connection::JRuby if RUBY_PLATFORM == 'java'
21
25
 
@@ -11,7 +11,6 @@ module ThinkingSphinx::Core::Index
11
11
 
12
12
  def initialize(reference, options = {})
13
13
  @reference = reference.to_sym
14
- @docinfo = :extern unless config.settings["skip_docinfo"]
15
14
  @options = options
16
15
  @offset = config.next_offset(options[:offset_as] || reference)
17
16
  @type = 'plain'
@@ -40,7 +39,7 @@ module ThinkingSphinx::Core::Index
40
39
  def interpret_definition!
41
40
  table_exists = model.table_exists?
42
41
  unless table_exists
43
- Rails.logger.info "No table exists for #{model}. Index can not be created"
42
+ Rails.logger.info "No table exists for #{model}. Index can not be created"
44
43
  return
45
44
  end
46
45
  return if @interpreted_definition
@@ -22,10 +22,6 @@ class ThinkingSphinx::Deletion
22
22
 
23
23
  attr_reader :index, :ids
24
24
 
25
- def document_ids_for_keys
26
- ids.collect { |id| index.document_id_for_key id }
27
- end
28
-
29
25
  def execute(statement)
30
26
  statement = statement.gsub(/\s*\n\s*/, ' ').strip
31
27
 
@@ -36,11 +32,28 @@ class ThinkingSphinx::Deletion
36
32
  end
37
33
  end
38
34
 
35
+ class PlainDeletion < ThinkingSphinx::Deletion
36
+ def perform
37
+ ids.each_slice(1000) do |some_ids|
38
+ execute <<-SQL
39
+ UPDATE #{name}
40
+ SET sphinx_deleted = 1
41
+ WHERE sphinx_internal_id IN (#{some_ids.join(', ')})
42
+ SQL
43
+ end
44
+ end
45
+ end
46
+
39
47
  class RealtimeDeletion < ThinkingSphinx::Deletion
40
48
  def perform
41
49
  return unless callbacks_enabled?
42
50
 
43
- execute Riddle::Query::Delete.new(name, document_ids_for_keys).to_sql
51
+ ids.each_slice(1000) do |some_ids|
52
+ execute <<-SQL
53
+ DELETE FROM #{name}
54
+ WHERE sphinx_internal_id IN (#{some_ids.join(', ')})
55
+ SQL
56
+ end
44
57
  end
45
58
 
46
59
  private
@@ -54,16 +67,4 @@ class ThinkingSphinx::Deletion
54
67
  ThinkingSphinx::Configuration.instance
55
68
  end
56
69
  end
57
-
58
- class PlainDeletion < ThinkingSphinx::Deletion
59
- def perform
60
- document_ids_for_keys.each_slice(1000) do |document_ids|
61
- execute <<-SQL
62
- UPDATE #{name}
63
- SET sphinx_deleted = 1
64
- WHERE id IN (#{document_ids.join(', ')})
65
- SQL
66
- end
67
- end
68
- end
69
70
  end
@@ -38,7 +38,7 @@ end
38
38
  class ThinkingSphinx::QueryLengthError < ThinkingSphinx::SphinxError
39
39
  def message
40
40
  <<-MESSAGE
41
- The supplied SphinxQL statement is #{statement.length} characters long. The maximum allowed length is #{ThinkingSphinx::MAXIMUM_STATEMENT_LENGTH}.
41
+ The supplied SphinxQL statement is #{statement.length} characters long. The maximum allowed length is #{ThinkingSphinx::Configuration.instance.settings['maximum_statement_length']}.
42
42
 
43
43
  If this error has been raised during real-time index population, it's probably due to overly large batches of records being processed at once. The default is 1000, but you can lower it on a per-environment basis in config/thinking_sphinx.yml:
44
44
 
@@ -34,11 +34,11 @@ class ThinkingSphinx::IndexSet
34
34
  end
35
35
 
36
36
  def classes
37
- options[:classes] || []
37
+ options[:classes] || instances.collect(&:class)
38
38
  end
39
39
 
40
40
  def classes_specified?
41
- classes.any? || references_specified?
41
+ instances.any? || classes.any? || references_specified?
42
42
  end
43
43
 
44
44
  def classes_and_ancestors
@@ -68,6 +68,10 @@ class ThinkingSphinx::IndexSet
68
68
  all_indices.select { |index| references.include? index.reference }
69
69
  end
70
70
 
71
+ def instances
72
+ options[:instances] || []
73
+ end
74
+
71
75
  def mti_classes
72
76
  classes.reject { |klass|
73
77
  klass.column_names.include?(klass.inheritance_column)
@@ -76,7 +80,7 @@ class ThinkingSphinx::IndexSet
76
80
 
77
81
  def references
78
82
  options[:references] || classes_and_ancestors.collect { |klass|
79
- ThinkingSphinx::IndexSet.reference_name(klass)
83
+ self.class.reference_name(klass)
80
84
  }
81
85
  end
82
86
 
@@ -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
 
@@ -5,21 +5,34 @@ class ThinkingSphinx::Railtie < Rails::Railtie
5
5
  ThinkingSphinx::Configuration.reset
6
6
  end
7
7
 
8
+ config.after_initialize do
9
+ require 'thinking_sphinx/active_record'
10
+ end
11
+
8
12
  initializer 'thinking_sphinx.initialisation' do
9
13
  ActiveSupport.on_load(:active_record) do
10
- ActiveRecord::Base.send :include, ThinkingSphinx::ActiveRecord::Base
14
+ require 'thinking_sphinx/active_record'
11
15
  end
12
16
 
13
- if ActiveSupport::VERSION::MAJOR > 5
14
- if Rails.application.config.autoloader == :zeitwerk
15
- ActiveSupport::Dependencies.autoload_paths.delete(
16
- Rails.root.join("app", "indices").to_s
17
- )
18
- end
17
+ if zeitwerk?
18
+ ActiveSupport::Dependencies.autoload_paths.delete(
19
+ Rails.root.join("app", "indices").to_s
20
+ )
19
21
  end
22
+
23
+ Rails.application.config.eager_load_paths -=
24
+ ThinkingSphinx::Configuration.instance.index_paths
25
+ Rails.application.config.eager_load_paths.freeze
20
26
  end
21
27
 
22
28
  rake_tasks do
23
29
  load File.expand_path('../tasks.rb', __FILE__)
24
30
  end
31
+
32
+ def zeitwerk?
33
+ return true if ActiveSupport::VERSION::MAJOR >= 7
34
+ return false if ActiveSupport::VERSION::MAJOR <= 5
35
+
36
+ Rails.application.config.autoloader == :zeitwerk
37
+ end
25
38
  end
@@ -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
@@ -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
@@ -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,6 +7,7 @@ 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
13
  def populate
@@ -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
@@ -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
 
@@ -9,6 +9,7 @@ class ThinkingSphinx::Search::StaleIdsException < StandardError
9
9
  end
10
10
 
11
11
  def message
12
- "Record IDs found by Sphinx but not by ActiveRecord : #{ids.join(', ')}"
12
+ "Record IDs found by Sphinx but not by ActiveRecord : #{ids.join(', ')}\n" \
13
+ "https://freelancing-gods.com/thinking-sphinx/v5/common_issues.html#record-ids"
13
14
  end
14
15
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  class ThinkingSphinx::Search < Array
4
4
  CORE_METHODS = %w( == class class_eval extend frozen? id instance_eval
5
- instance_of? instance_values instance_variable_defined?
5
+ instance_exec instance_of? instance_values instance_variable_defined?
6
6
  instance_variable_get instance_variable_set instance_variables is_a?
7
7
  kind_of? member? method methods nil? object_id respond_to?
8
8
  respond_to_missing? send should should_not type )