thinking-sphinx 3.3.0 → 3.4.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +29 -20
  4. data/Appraisals +9 -5
  5. data/Gemfile +8 -3
  6. data/HISTORY +24 -0
  7. data/README.textile +5 -4
  8. data/bin/console +14 -0
  9. data/bin/literals +9 -0
  10. data/bin/loadsphinx +38 -0
  11. data/lib/thinking_sphinx.rb +15 -2
  12. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +2 -3
  13. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +11 -1
  14. data/lib/thinking_sphinx/active_record/index.rb +1 -1
  15. data/lib/thinking_sphinx/active_record/join_association.rb +3 -1
  16. data/lib/thinking_sphinx/active_record/log_subscriber.rb +5 -0
  17. data/lib/thinking_sphinx/active_record/sql_source.rb +1 -1
  18. data/lib/thinking_sphinx/attribute_types.rb +70 -0
  19. data/lib/thinking_sphinx/commands/base.rb +41 -0
  20. data/lib/thinking_sphinx/commands/configure.rb +13 -0
  21. data/lib/thinking_sphinx/commands/index.rb +11 -0
  22. data/lib/thinking_sphinx/commands/start_attached.rb +20 -0
  23. data/lib/thinking_sphinx/commands/start_detached.rb +19 -0
  24. data/lib/thinking_sphinx/commands/stop.rb +22 -0
  25. data/lib/thinking_sphinx/configuration.rb +36 -28
  26. data/lib/thinking_sphinx/configuration/minimum_fields.rb +11 -8
  27. data/lib/thinking_sphinx/connection.rb +5 -122
  28. data/lib/thinking_sphinx/connection/client.rb +48 -0
  29. data/lib/thinking_sphinx/connection/jruby.rb +53 -0
  30. data/lib/thinking_sphinx/connection/mri.rb +28 -0
  31. data/lib/thinking_sphinx/core/index.rb +11 -0
  32. data/lib/thinking_sphinx/deletion.rb +6 -2
  33. data/lib/thinking_sphinx/deltas/default_delta.rb +1 -1
  34. data/lib/thinking_sphinx/deltas/delete_job.rb +14 -4
  35. data/lib/thinking_sphinx/distributed/index.rb +10 -0
  36. data/lib/thinking_sphinx/errors.rb +1 -1
  37. data/lib/thinking_sphinx/index_set.rb +14 -2
  38. data/lib/thinking_sphinx/interfaces/daemon.rb +32 -0
  39. data/lib/thinking_sphinx/interfaces/real_time.rb +41 -0
  40. data/lib/thinking_sphinx/interfaces/sql.rb +41 -0
  41. data/lib/thinking_sphinx/middlewares.rb +5 -3
  42. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +13 -6
  43. data/lib/thinking_sphinx/middlewares/attribute_typer.rb +48 -0
  44. data/lib/thinking_sphinx/middlewares/valid_options.rb +23 -0
  45. data/lib/thinking_sphinx/rake_interface.rb +10 -124
  46. data/lib/thinking_sphinx/search.rb +11 -0
  47. data/lib/thinking_sphinx/search/query.rb +7 -1
  48. data/lib/thinking_sphinx/tasks.rb +80 -21
  49. data/lib/thinking_sphinx/with_output.rb +11 -0
  50. data/spec/acceptance/connection_spec.rb +4 -4
  51. data/spec/acceptance/searching_within_a_model_spec.rb +7 -0
  52. data/spec/acceptance/specifying_sql_spec.rb +26 -8
  53. data/spec/acceptance/sql_deltas_spec.rb +12 -0
  54. data/spec/internal/app/indices/album_index.rb +3 -0
  55. data/spec/internal/app/models/album.rb +19 -0
  56. data/spec/internal/db/schema.rb +8 -0
  57. data/spec/spec_helper.rb +4 -0
  58. data/spec/support/json_column.rb +5 -1
  59. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +5 -1
  60. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +6 -0
  61. data/spec/thinking_sphinx/attribute_types_spec.rb +50 -0
  62. data/spec/thinking_sphinx/commands/configure_spec.rb +29 -0
  63. data/spec/thinking_sphinx/commands/index_spec.rb +26 -0
  64. data/spec/thinking_sphinx/commands/start_detached_spec.rb +55 -0
  65. data/spec/thinking_sphinx/commands/stop_spec.rb +54 -0
  66. data/spec/thinking_sphinx/configuration/minimum_fields_spec.rb +36 -0
  67. data/spec/thinking_sphinx/deletion_spec.rb +2 -5
  68. data/spec/thinking_sphinx/deltas/default_delta_spec.rb +1 -1
  69. data/spec/thinking_sphinx/errors_spec.rb +7 -0
  70. data/spec/thinking_sphinx/index_set_spec.rb +30 -7
  71. data/spec/thinking_sphinx/interfaces/daemon_spec.rb +52 -0
  72. data/spec/thinking_sphinx/interfaces/real_time_spec.rb +109 -0
  73. data/spec/thinking_sphinx/interfaces/sql_spec.rb +98 -0
  74. data/spec/thinking_sphinx/middlewares/attribute_typer_spec.rb +42 -0
  75. data/spec/thinking_sphinx/middlewares/valid_options_spec.rb +49 -0
  76. data/spec/thinking_sphinx/rake_interface_spec.rb +13 -246
  77. data/spec/thinking_sphinx/search/query_spec.rb +7 -0
  78. data/thinking-sphinx.gemspec +5 -4
  79. metadata +72 -16
  80. data/gemfiles/.gitignore +0 -1
  81. data/gemfiles/rails_3_2.gemfile +0 -13
  82. data/gemfiles/rails_4_0.gemfile +0 -13
  83. data/gemfiles/rails_4_1.gemfile +0 -13
  84. data/gemfiles/rails_4_2.gemfile +0 -13
  85. data/gemfiles/rails_5_0.gemfile +0 -12
@@ -0,0 +1,41 @@
1
+ class ThinkingSphinx::Commands::Base
2
+ include ThinkingSphinx::WithOutput
3
+
4
+ def self.call(configuration, options, stream = STDOUT)
5
+ new(configuration, options, stream).call_with_handling
6
+ end
7
+
8
+ def call_with_handling
9
+ call
10
+ rescue Riddle::CommandFailedError => error
11
+ handle_failure error.command_result
12
+ end
13
+
14
+ private
15
+
16
+ delegate :controller, :to => :configuration
17
+
18
+ def command_output(output)
19
+ return "See above\n" if output.nil?
20
+
21
+ "\n\t" + output.gsub("\n", "\n\t")
22
+ end
23
+
24
+ def handle_failure(result)
25
+ stream.puts <<-TXT
26
+
27
+ The Sphinx #{type} command failed:
28
+ Command: #{result.command}
29
+ Status: #{result.status}
30
+ Output: #{command_output result.output}
31
+ There may be more information about the failure in #{configuration.searchd.log}.
32
+ TXT
33
+ exit result.status
34
+ end
35
+
36
+ def log(message)
37
+ return if options[:silent]
38
+
39
+ stream.puts message
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ class ThinkingSphinx::Commands::Configure < ThinkingSphinx::Commands::Base
2
+ def call
3
+ log "Generating configuration to #{configuration.configuration_file}"
4
+
5
+ configuration.render_to_file
6
+ end
7
+
8
+ private
9
+
10
+ def type
11
+ 'configure'
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ class ThinkingSphinx::Commands::Index < ThinkingSphinx::Commands::Base
2
+ def call
3
+ controller.index :verbose => options[:verbose]
4
+ end
5
+
6
+ private
7
+
8
+ def type
9
+ 'indexing'
10
+ end
11
+ end
@@ -0,0 +1,20 @@
1
+ class ThinkingSphinx::Commands::StartAttached < ThinkingSphinx::Commands::Base
2
+ def call
3
+ FileUtils.mkdir_p configuration.indices_location
4
+
5
+ unless pid = fork
6
+ controller.start :verbose => options[:verbose], :nodetach => true
7
+ end
8
+
9
+ Signal.trap('TERM') { Process.kill(:TERM, pid) }
10
+ Signal.trap('INT') { Process.kill(:TERM, pid) }
11
+
12
+ Process.wait(pid)
13
+ end
14
+
15
+ private
16
+
17
+ def type
18
+ 'start'
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ class ThinkingSphinx::Commands::StartDetached < ThinkingSphinx::Commands::Base
2
+ def call
3
+ FileUtils.mkdir_p configuration.indices_location
4
+
5
+ result = controller.start :verbose => options[:verbose]
6
+
7
+ if controller.running?
8
+ log "Started searchd successfully (pid: #{controller.pid})."
9
+ else
10
+ handle_failure result
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def type
17
+ 'start'
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ class ThinkingSphinx::Commands::Stop < ThinkingSphinx::Commands::Base
2
+ def call
3
+ unless controller.running?
4
+ log 'searchd is not currently running.'
5
+ return
6
+ end
7
+
8
+ pid = controller.pid
9
+ until !controller.running? do
10
+ controller.stop options
11
+ sleep(0.5)
12
+ end
13
+
14
+ log "Stopped searchd daemon (pid: #{pid})."
15
+ end
16
+
17
+ private
18
+
19
+ def type
20
+ 'stop'
21
+ end
22
+ end
@@ -84,9 +84,8 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
84
84
  end
85
85
  end
86
86
 
87
- if settings['distributed_indices'].nil? || settings['distributed_indices']
88
- ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
89
- end
87
+ normalise
88
+ verify
90
89
 
91
90
  @preloaded_indices = true
92
91
  end
@@ -95,10 +94,6 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
95
94
  def render
96
95
  preload_indices
97
96
 
98
- ThinkingSphinx::Configuration::ConsistentIds.new(indices).reconcile
99
- ThinkingSphinx::Configuration::MinimumFields.new(indices).reconcile
100
- ThinkingSphinx::Configuration::DuplicateNames.new(indices).reconcile
101
-
102
97
  super
103
98
  end
104
99
 
@@ -137,6 +132,16 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
137
132
 
138
133
  private
139
134
 
135
+ def apply_sphinx_settings!
136
+ sphinx_sections.each do |object|
137
+ settings.each do |key, value|
138
+ next unless object.class.settings.include?(key.to_sym)
139
+
140
+ object.send("#{key}=", value)
141
+ end
142
+ end
143
+ end
144
+
140
145
  def configure_searchd
141
146
  configure_searchd_log_files
142
147
 
@@ -153,12 +158,21 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
153
158
  searchd.query_log = log_root.join("#{environment}.searchd.query.log").to_s
154
159
  end
155
160
 
161
+ def framework_root
162
+ Pathname.new(framework.root)
163
+ end
164
+
156
165
  def log_root
157
166
  real_path 'log'
158
167
  end
159
168
 
160
- def framework_root
161
- Pathname.new(framework.root)
169
+ def normalise
170
+ if settings['distributed_indices'].nil? || settings['distributed_indices']
171
+ ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile
172
+ end
173
+
174
+ ThinkingSphinx::Configuration::ConsistentIds.new(indices).reconcile
175
+ ThinkingSphinx::Configuration::MinimumFields.new(indices).reconcile
162
176
  end
163
177
 
164
178
  def real_path(*arguments)
@@ -166,25 +180,21 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
166
180
  path.exist? ? path.realpath : path
167
181
  end
168
182
 
169
- def settings_to_hash
170
- input = File.read settings_file
171
- input = ERB.new(input).result if defined?(ERB)
172
-
173
- contents = YAML.load input
174
- contents && contents[environment] || {}
183
+ def reset
184
+ @settings = nil
185
+ setup
175
186
  end
176
187
 
177
188
  def settings_file
178
189
  framework_root.join 'config', 'thinking_sphinx.yml'
179
190
  end
180
191
 
181
- def reset
182
- @settings = nil
183
- setup
184
- end
192
+ def settings_to_hash
193
+ input = File.read settings_file
194
+ input = ERB.new(input).result if defined?(ERB)
185
195
 
186
- def tmp_path
187
- real_path 'tmp'
196
+ contents = YAML.load input
197
+ contents && contents[environment] || {}
188
198
  end
189
199
 
190
200
  def sphinx_sections
@@ -193,14 +203,12 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
193
203
  sections
194
204
  end
195
205
 
196
- def apply_sphinx_settings!
197
- sphinx_sections.each do |object|
198
- settings.each do |key, value|
199
- next unless object.class.settings.include?(key.to_sym)
206
+ def tmp_path
207
+ real_path 'tmp'
208
+ end
200
209
 
201
- object.send("#{key}=", value)
202
- end
203
- end
210
+ def verify
211
+ ThinkingSphinx::Configuration::DuplicateNames.new(indices).reconcile
204
212
  end
205
213
  end
206
214
 
@@ -6,8 +6,8 @@ class ThinkingSphinx::Configuration::MinimumFields
6
6
  def reconcile
7
7
  return unless no_inheritance_columns?
8
8
 
9
- sources.each do |source|
10
- source.fields.delete_if do |field|
9
+ field_collections.each do |collection|
10
+ collection.fields.delete_if do |field|
11
11
  field.name == 'sphinx_internal_class_name'
12
12
  end
13
13
  end
@@ -17,15 +17,18 @@ class ThinkingSphinx::Configuration::MinimumFields
17
17
 
18
18
  attr_reader :indices
19
19
 
20
+ def field_collections
21
+ indices_of_type('plain').collect(&:sources).flatten +
22
+ indices_of_type('rt')
23
+ end
24
+
25
+ def indices_of_type(type)
26
+ indices.select { |index| index.type == type }
27
+ end
28
+
20
29
  def no_inheritance_columns?
21
30
  indices.select { |index|
22
31
  index.model.column_names.include?(index.model.inheritance_column)
23
32
  }.empty?
24
33
  end
25
-
26
- def sources
27
- @sources ||= @indices.select { |index|
28
- index.respond_to?(:sources)
29
- }.collect(&:sources).flatten
30
- end
31
34
  end
@@ -26,7 +26,7 @@ module ThinkingSphinx::Connection
26
26
  def self.pool
27
27
  @pool ||= Innertube::Pool.new(
28
28
  Proc.new { ThinkingSphinx::Connection.new },
29
- Proc.new { |connection| connection.close }
29
+ Proc.new { |connection| connection.close! }
30
30
  )
31
31
  end
32
32
 
@@ -63,125 +63,8 @@ module ThinkingSphinx::Connection
63
63
  end
64
64
 
65
65
  @persistent = true
66
-
67
- class Client
68
- def close
69
- client.close unless ThinkingSphinx::Connection.persistent?
70
- end
71
-
72
- def execute(statement)
73
- check_and_perform(statement).first
74
- end
75
-
76
- def query_all(*statements)
77
- check_and_perform statements.join('; ')
78
- end
79
-
80
- private
81
-
82
- def check(statements)
83
- if statements.length > ThinkingSphinx::MAXIMUM_STATEMENT_LENGTH
84
- exception = ThinkingSphinx::QueryLengthError.new
85
- exception.statement = statements
86
- raise exception
87
- end
88
- end
89
-
90
- def check_and_perform(statements)
91
- check statements
92
- perform statements
93
- end
94
-
95
- def close_and_clear
96
- client.close
97
- @client = nil
98
- end
99
-
100
- def perform(statements)
101
- results_for statements
102
- rescue => error
103
- message = "#{error.message} - #{statements}"
104
- wrapper = ThinkingSphinx::QueryExecutionError.new message
105
- wrapper.statement = statements
106
- raise wrapper
107
- ensure
108
- close_and_clear unless ThinkingSphinx::Connection.persistent?
109
- end
110
- end
111
-
112
- class MRI < Client
113
- def initialize(options)
114
- @options = options
115
- end
116
-
117
- def base_error
118
- Mysql2::Error
119
- end
120
-
121
- private
122
-
123
- attr_reader :options
124
-
125
- def client
126
- @client ||= Mysql2::Client.new({
127
- :flags => Mysql2::Client::MULTI_STATEMENTS
128
- }.merge(options))
129
- rescue base_error => error
130
- raise ThinkingSphinx::SphinxError.new_from_mysql error
131
- end
132
-
133
- def results_for(statements)
134
- results = [client.query(statements)]
135
- results << client.store_result while client.next_result
136
- results
137
- end
138
- end
139
-
140
- class JRuby < Client
141
- attr_reader :address, :options
142
-
143
- def initialize(options)
144
- @address = "jdbc:mysql://#{options[:host]}:#{options[:port]}/?allowMultiQueries=true"
145
- @options = options
146
- end
147
-
148
- def base_error
149
- Java::JavaSql::SQLException
150
- end
151
-
152
- private
153
-
154
- def client
155
- @client ||= java.sql.DriverManager.getConnection address,
156
- options[:username], options[:password]
157
- rescue base_error => error
158
- raise ThinkingSphinx::SphinxError.new_from_mysql error
159
- end
160
-
161
- def results_for(statements)
162
- statement = client.createStatement
163
- statement.execute statements
164
-
165
- results = [set_to_array(statement.getResultSet)]
166
- results << set_to_array(statement.getResultSet) while statement.getMoreResults
167
- results.compact
168
- end
169
-
170
- def set_to_array(set)
171
- return nil if set.nil?
172
-
173
- meta = set.getMetaData
174
- rows = []
175
-
176
- while set.next
177
- rows << (1..meta.getColumnCount).inject({}) do |row, index|
178
- name = meta.getColumnName index
179
- row[name] = set.getObject(index)
180
- row
181
- end
182
- end
183
-
184
- rows
185
- end
186
- end
187
66
  end
67
+
68
+ require 'thinking_sphinx/connection/client'
69
+ require 'thinking_sphinx/connection/jruby'
70
+ require 'thinking_sphinx/connection/mri'
@@ -0,0 +1,48 @@
1
+ class ThinkingSphinx::Connection::Client
2
+ def close
3
+ close! unless ThinkingSphinx::Connection.persistent?
4
+ end
5
+
6
+ def close!
7
+ client.close
8
+ end
9
+
10
+ def execute(statement)
11
+ check_and_perform(statement).first
12
+ end
13
+
14
+ def query_all(*statements)
15
+ check_and_perform statements.join('; ')
16
+ end
17
+
18
+ private
19
+
20
+ def check(statements)
21
+ if statements.length > ThinkingSphinx::MAXIMUM_STATEMENT_LENGTH
22
+ exception = ThinkingSphinx::QueryLengthError.new
23
+ exception.statement = statements
24
+ raise exception
25
+ end
26
+ end
27
+
28
+ def check_and_perform(statements)
29
+ check statements
30
+ perform statements
31
+ end
32
+
33
+ def close_and_clear
34
+ client.close
35
+ @client = nil
36
+ end
37
+
38
+ def perform(statements)
39
+ results_for statements
40
+ rescue => error
41
+ message = "#{error.message} - #{statements}"
42
+ wrapper = ThinkingSphinx::QueryExecutionError.new message
43
+ wrapper.statement = statements
44
+ raise wrapper
45
+ ensure
46
+ close_and_clear unless ThinkingSphinx::Connection.persistent?
47
+ end
48
+ end