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
@@ -48,6 +48,6 @@ class ThinkingSphinx::ActiveRecord::SQLSource::Template
48
48
  end
49
49
 
50
50
  def primary_key
51
- source.model.primary_key.to_sym
51
+ source.options[:primary_key].to_sym
52
52
  end
53
53
  end
@@ -9,6 +9,24 @@ class ThinkingSphinx::Callbacks
9
9
  extend mod
10
10
  end
11
11
 
12
+ def self.resume!
13
+ @suspended = false
14
+ end
15
+
16
+ def self.suspend(&block)
17
+ suspend!
18
+ yield
19
+ resume!
20
+ end
21
+
22
+ def self.suspend!
23
+ @suspended = true
24
+ end
25
+
26
+ def self.suspended?
27
+ @suspended
28
+ end
29
+
12
30
  def initialize(instance)
13
31
  @instance = instance
14
32
  end
@@ -3,14 +3,16 @@ require 'pathname'
3
3
  class ThinkingSphinx::Configuration < Riddle::Configuration
4
4
  attr_accessor :configuration_file, :indices_location, :version
5
5
  attr_reader :index_paths
6
- attr_writer :controller, :index_set_class
6
+ attr_writer :controller, :index_set_class, :indexing_strategy
7
7
 
8
8
  delegate :environment, :to => :framework
9
9
 
10
+ @@mutex = Mutex.new
11
+
10
12
  def initialize
11
13
  super
12
14
 
13
- setup
15
+ reset
14
16
  end
15
17
 
16
18
  def self.instance
@@ -39,7 +41,7 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
39
41
 
40
42
  def framework=(framework)
41
43
  @framework = framework
42
- setup
44
+ reset
43
45
  framework
44
46
  end
45
47
 
@@ -60,6 +62,10 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
60
62
  @index_set_class ||= ThinkingSphinx::IndexSet
61
63
  end
62
64
 
65
+ def indexing_strategy
66
+ @indexing_strategy ||= ThinkingSphinx::IndexingStrategies::AllAtOnce
67
+ end
68
+
63
69
  def indices_for_references(*references)
64
70
  index_set_class.new(:references => references).to_a
65
71
  end
@@ -69,19 +75,21 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
69
75
  end
70
76
 
71
77
  def preload_indices
72
- return if @preloaded_indices
78
+ @@mutex.synchronize do
79
+ return if @preloaded_indices
73
80
 
74
- index_paths.each do |path|
75
- Dir["#{path}/**/*.rb"].sort.each do |file|
76
- ActiveSupport::Dependencies.require_or_load file
81
+ index_paths.each do |path|
82
+ Dir["#{path}/**/*.rb"].sort.each do |file|
83
+ ActiveSupport::Dependencies.require_or_load file
84
+ end
77
85
  end
78
- end
79
86
 
80
- if settings['distributed_indices'].nil? || settings['distributed_indices']
81
- ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
82
- end
87
+ if settings['distributed_indices'].nil? || settings['distributed_indices']
88
+ ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
89
+ end
83
90
 
84
- @preloaded_indices = true
91
+ @preloaded_indices = true
92
+ end
85
93
  end
86
94
 
87
95
  def render
@@ -89,6 +97,7 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
89
97
 
90
98
  ThinkingSphinx::Configuration::ConsistentIds.new(indices).reconcile
91
99
  ThinkingSphinx::Configuration::MinimumFields.new(indices).reconcile
100
+ ThinkingSphinx::Configuration::DuplicateNames.new(indices).reconcile
92
101
 
93
102
  super
94
103
  end
@@ -103,6 +112,28 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
103
112
  @settings ||= File.exists?(settings_file) ? settings_to_hash : {}
104
113
  end
105
114
 
115
+ def setup
116
+ @configuration_file = settings['configuration_file'] || framework_root.join(
117
+ 'config', "#{environment}.sphinx.conf"
118
+ ).to_s
119
+ @index_paths = engine_index_paths + [framework_root.join('app', 'indices').to_s]
120
+ @indices_location = settings['indices_location'] || framework_root.join(
121
+ 'db', 'sphinx', environment
122
+ ).to_s
123
+ @version = settings['version'] || '2.1.4'
124
+
125
+ if settings['common_sphinx_configuration']
126
+ common.common_sphinx_configuration = true
127
+ indexer.common_sphinx_configuration = true
128
+ end
129
+
130
+ configure_searchd
131
+
132
+ apply_sphinx_settings!
133
+
134
+ @offsets = {}
135
+ end
136
+
106
137
  private
107
138
 
108
139
  def configure_searchd
@@ -135,7 +166,10 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
135
166
  end
136
167
 
137
168
  def settings_to_hash
138
- contents = YAML.load(ERB.new(File.read(settings_file)).result)
169
+ input = File.read settings_file
170
+ input = ERB.new(input).result if defined?(ERB)
171
+
172
+ contents = YAML.load input
139
173
  contents && contents[environment] || {}
140
174
  end
141
175
 
@@ -143,27 +177,9 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
143
177
  framework_root.join 'config', 'thinking_sphinx.yml'
144
178
  end
145
179
 
146
- def setup
180
+ def reset
147
181
  @settings = nil
148
- @configuration_file = settings['configuration_file'] || framework_root.join(
149
- 'config', "#{environment}.sphinx.conf"
150
- ).to_s
151
- @index_paths = engine_index_paths + [framework_root.join('app', 'indices').to_s]
152
- @indices_location = settings['indices_location'] || framework_root.join(
153
- 'db', 'sphinx', environment
154
- ).to_s
155
- @version = settings['version'] || '2.1.4'
156
-
157
- if settings['common_sphinx_configuration']
158
- common.common_sphinx_configuration = true
159
- indexer.common_sphinx_configuration = true
160
- end
161
-
162
- configure_searchd
163
-
164
- apply_sphinx_settings!
165
-
166
- @offsets = {}
182
+ setup
167
183
  end
168
184
 
169
185
  def tmp_path
@@ -190,4 +206,5 @@ end
190
206
  require 'thinking_sphinx/configuration/consistent_ids'
191
207
  require 'thinking_sphinx/configuration/defaults'
192
208
  require 'thinking_sphinx/configuration/distributed_indices'
209
+ require 'thinking_sphinx/configuration/duplicate_names'
193
210
  require 'thinking_sphinx/configuration/minimum_fields'
@@ -0,0 +1,34 @@
1
+ class ThinkingSphinx::Configuration::DuplicateNames
2
+ def initialize(indices)
3
+ @indices = indices
4
+ end
5
+
6
+ def reconcile
7
+ indices.each do |index|
8
+ return if index.distributed?
9
+
10
+ counts_for(index).each do |name, count|
11
+ next if count <= 1
12
+
13
+ raise ThinkingSphinx::DuplicateNameError,
14
+ "Duplicate field/attribute name '#{name}' in index '#{index.name}'"
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :indices
22
+
23
+ def counts_for(index)
24
+ names_for(index).inject({}) do |hash, name|
25
+ hash[name] ||= 0
26
+ hash[name] += 1
27
+ hash
28
+ end
29
+ end
30
+
31
+ def names_for(index)
32
+ index.fields.collect(&:name) + index.attributes.collect(&:name)
33
+ end
34
+ end
@@ -157,13 +157,13 @@ module ThinkingSphinx::Connection
157
157
  def set_to_array(set)
158
158
  return nil if set.nil?
159
159
 
160
- meta = set.meta_data
160
+ meta = set.getMetaData
161
161
  rows = []
162
162
 
163
163
  while set.next
164
- rows << (1..meta.column_count).inject({}) do |row, index|
165
- name = meta.column_name index
166
- row[name] = set.get_object(index)
164
+ rows << (1..meta.getColumnCount).inject({}) do |row, index|
165
+ name = meta.getColumnName index
166
+ row[name] = set.getObject(index)
167
167
  row
168
168
  end
169
169
  end
@@ -1,10 +1,12 @@
1
1
  class ThinkingSphinx::Controller < Riddle::Controller
2
2
  def index(*indices)
3
- options = indices.extract_options!
4
- indices << '--all' if indices.empty?
3
+ configuration = ThinkingSphinx::Configuration.instance
4
+ options = indices.extract_options!
5
5
 
6
- ThinkingSphinx::Guard::Files.call(indices) do |names|
7
- super(*(names + [options]))
6
+ configuration.indexing_strategy.call(indices) do |index_names|
7
+ ThinkingSphinx::Guard::Files.call(index_names) do |names|
8
+ super(*(names + [options]))
9
+ end
8
10
  end
9
11
  end
10
12
  end
@@ -32,8 +32,21 @@ class ThinkingSphinx::Deletion
32
32
 
33
33
  class RealtimeDeletion < ThinkingSphinx::Deletion
34
34
  def perform
35
+ return unless callbacks_enabled?
36
+
35
37
  execute Riddle::Query::Delete.new(name, document_ids_for_keys).to_sql
36
38
  end
39
+
40
+ private
41
+
42
+ def callbacks_enabled?
43
+ setting = configuration.settings['real_time_callbacks']
44
+ setting.nil? || setting
45
+ end
46
+
47
+ def configuration
48
+ ThinkingSphinx::Configuration.instance
49
+ end
37
50
  end
38
51
 
39
52
  class PlainDeletion < ThinkingSphinx::Deletion
@@ -13,6 +13,8 @@ class ThinkingSphinx::SphinxError < StandardError
13
13
  replacement = ThinkingSphinx::ConnectionError.new(
14
14
  "Error connecting to Sphinx via the MySQL protocol. #{error.message}"
15
15
  )
16
+ when /offset out of bounds/
17
+ replacement = ThinkingSphinx::OutOfBoundsError.new(error.message)
16
18
  else
17
19
  replacement = new(error.message)
18
20
  end
@@ -35,6 +37,9 @@ end
35
37
  class ThinkingSphinx::ParseError < ThinkingSphinx::QueryError
36
38
  end
37
39
 
40
+ class ThinkingSphinx::OutOfBoundsError < ThinkingSphinx::QueryError
41
+ end
42
+
38
43
  class ThinkingSphinx::QueryExecutionError < StandardError
39
44
  attr_accessor :statement
40
45
  end
@@ -50,3 +55,6 @@ end
50
55
 
51
56
  class ThinkingSphinx::PopulatedResultsError < StandardError
52
57
  end
58
+
59
+ class ThinkingSphinx::DuplicateNameError < StandardError
60
+ end
@@ -1,5 +1,10 @@
1
1
  class ThinkingSphinx::IndexSet
2
2
  include Enumerable
3
+
4
+ def self.reference_name(klass)
5
+ @cached_results ||= {}
6
+ @cached_results[klass.name] ||= klass.name.underscore.to_sym
7
+ end
3
8
 
4
9
  delegate :each, :empty?, :to => :indices
5
10
 
@@ -63,7 +68,7 @@ class ThinkingSphinx::IndexSet
63
68
 
64
69
  def references
65
70
  options[:references] || classes_and_ancestors.collect { |klass|
66
- klass.name.underscore.to_sym
71
+ ThinkingSphinx::IndexSet.reference_name(klass)
67
72
  }
68
73
  end
69
74
 
@@ -0,0 +1,7 @@
1
+ class ThinkingSphinx::IndexingStrategies::AllAtOnce
2
+ def self.call(indices = [], &block)
3
+ indices << '--all' if indices.empty?
4
+
5
+ block.call indices
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ class ThinkingSphinx::IndexingStrategies::OneAtATime
2
+ def self.call(indices = [], &block)
3
+ if indices.empty?
4
+ configuration = ThinkingSphinx::Configuration.instance
5
+ configuration.preload_indices
6
+
7
+ indices = configuration.indices.select { |index|
8
+ !(index.distributed? || index.type == 'rt')
9
+ }.collect &:name
10
+ end
11
+
12
+ indices.each { |name| block.call [name] }
13
+ end
14
+ end
@@ -57,7 +57,7 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
57
57
  @results_for_models ||= model_names.inject({}) do |hash, name|
58
58
  model = name.constantize
59
59
  hash[name] = model_relation_with_sql_options(model.unscoped).where(
60
- model.primary_key => ids_for_model(name)
60
+ (context.configuration.settings[:primary_key] || model.primary_key || :id) => ids_for_model(name)
61
61
  )
62
62
 
63
63
  hash
@@ -45,7 +45,7 @@ class ThinkingSphinx::Middlewares::Inquirer <
45
45
 
46
46
  def call(raw_results, meta_results)
47
47
  context[:results] = raw_results.to_a
48
- context[:raw] = raw_results
48
+ context[:raw] = context[:results].dup
49
49
  context[:meta] = meta_results.inject({}) { |hash, row|
50
50
  hash[row['Variable_name']] = row['Value']
51
51
  hash
@@ -4,7 +4,7 @@ class ThinkingSphinx::Middlewares::SphinxQL <
4
4
  SELECT_OPTIONS = [:agent_query_timeout, :boolean_simplify, :comment, :cutoff,
5
5
  :field_weights, :global_idf, :idf, :index_weights, :max_matches,
6
6
  :max_query_time, :max_predicted_time, :ranker, :retry_count, :retry_delay,
7
- :reverse_scan, :sort_method]
7
+ :reverse_scan, :sort_method, :rand_seed]
8
8
 
9
9
  def call(contexts)
10
10
  contexts.each do |context|
@@ -82,7 +82,7 @@ class ThinkingSphinx::Middlewares::SphinxQL <
82
82
 
83
83
  def indices_match_classes?
84
84
  indices.collect(&:reference).uniq.sort == classes.collect { |klass|
85
- klass.name.underscore.to_sym
85
+ ThinkingSphinx::IndexSet.reference_name(klass)
86
86
  }.sort
87
87
  end
88
88
 
@@ -33,7 +33,7 @@ class ThinkingSphinx::Middlewares::StaleIdChecker <
33
33
  end
34
34
 
35
35
  def raise_exception
36
- raise ThinkingSphinx::Search::StaleIdsException, stale_ids
36
+ raise ThinkingSphinx::Search::StaleIdsException.new(stale_ids, context)
37
37
  end
38
38
 
39
39
  def stale_ids
@@ -11,7 +11,7 @@ class ThinkingSphinx::Middlewares::StaleIdFilter <
11
11
  rescue ThinkingSphinx::Search::StaleIdsException => error
12
12
  raise error if @retries <= 0
13
13
 
14
- append_stale_ids error.ids
14
+ append_stale_ids error.ids, error.context
15
15
  ThinkingSphinx::Logger.log :message, log_message
16
16
 
17
17
  @retries -= 1 and retry
@@ -20,7 +20,7 @@ class ThinkingSphinx::Middlewares::StaleIdFilter <
20
20
 
21
21
  private
22
22
 
23
- def append_stale_ids(ids)
23
+ def append_stale_ids(ids, context)
24
24
  @stale_ids |= ids
25
25
 
26
26
  context.search.options[:without_ids] ||= []
@@ -45,16 +45,25 @@ class ThinkingSphinx::RakeInterface
45
45
  FileUtils.mkdir_p configuration.indices_location
46
46
  end
47
47
 
48
- def start
48
+ def start(options={})
49
49
  raise RuntimeError, 'searchd is already running' if controller.running?
50
50
 
51
51
  FileUtils.mkdir_p configuration.indices_location
52
- controller.start
53
52
 
54
- if controller.running?
55
- puts "Started searchd successfully (pid: #{controller.pid})."
53
+ if options[:nodetach]
54
+ unless pid = fork
55
+ controller.start(options)
56
+ end
57
+ Signal.trap('TERM') { Process.kill(:TERM, pid); }
58
+ Signal.trap('INT') { Process.kill(:TERM, pid); }
59
+ Process.wait(pid)
56
60
  else
57
- puts "Failed to start searchd. Check the log files for more information."
61
+ controller.start(options)
62
+ if controller.running?
63
+ puts "Started searchd successfully (pid: #{controller.pid})."
64
+ else
65
+ puts "Failed to start searchd. Check the log files for more information."
66
+ end
58
67
  end
59
68
  end
60
69