thinking-sphinx 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
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,53 @@
1
+ class ThinkingSphinx::Connection::JRuby < ThinkingSphinx::Connection::Client
2
+ attr_reader :address, :options
3
+
4
+ def initialize(options)
5
+ @address = "jdbc:mysql://#{options[:host]}:#{options[:port]}/?allowMultiQueries=true"
6
+ @options = options
7
+ end
8
+
9
+ def base_error
10
+ Java::JavaSql::SQLException
11
+ end
12
+
13
+ private
14
+
15
+ def client
16
+ @client ||= Java::ComMysqlJdbc::Driver.new.connect address, properties
17
+ rescue base_error => error
18
+ raise ThinkingSphinx::SphinxError.new_from_mysql error
19
+ end
20
+
21
+ def properties
22
+ object = Java::JavaUtil::Properties.new
23
+ object.setProperty "user", options[:username] if options[:username]
24
+ object.setProperty "password", options[:password] if options[:password]
25
+ object
26
+ end
27
+
28
+ def results_for(statements)
29
+ statement = client.createStatement
30
+ statement.execute statements
31
+
32
+ results = [set_to_array(statement.getResultSet)]
33
+ results << set_to_array(statement.getResultSet) while statement.getMoreResults
34
+ results.compact
35
+ end
36
+
37
+ def set_to_array(set)
38
+ return nil if set.nil?
39
+
40
+ meta = set.getMetaData
41
+ rows = []
42
+
43
+ while set.next
44
+ rows << (1..meta.getColumnCount).inject({}) do |row, index|
45
+ name = meta.getColumnName index
46
+ row[name] = set.getObject(index)
47
+ row
48
+ end
49
+ end
50
+
51
+ rows
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ class ThinkingSphinx::Connection::MRI < ThinkingSphinx::Connection::Client
2
+ def initialize(options)
3
+ @options = options
4
+ end
5
+
6
+ def base_error
7
+ Mysql2::Error
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :options
13
+
14
+ def client
15
+ @client ||= Mysql2::Client.new({
16
+ :flags => Mysql2::Client::MULTI_STATEMENTS,
17
+ :connect_timeout => 5
18
+ }.merge(options))
19
+ rescue base_error => error
20
+ raise ThinkingSphinx::SphinxError.new_from_mysql error
21
+ end
22
+
23
+ def results_for(statements)
24
+ results = [client.query(statements)]
25
+ results << client.store_result while client.next_result
26
+ results
27
+ end
28
+ end
@@ -25,7 +25,13 @@ module ThinkingSphinx::Core::Index
25
25
  false
26
26
  end
27
27
 
28
+ def document_id_for_instance(instance)
29
+ document_id_for_key instance.public_send(primary_key)
30
+ end
31
+
28
32
  def document_id_for_key(key)
33
+ return nil if key.nil?
34
+
29
35
  key * config.indices.count + offset
30
36
  end
31
37
 
@@ -47,6 +53,11 @@ module ThinkingSphinx::Core::Index
47
53
  @options
48
54
  end
49
55
 
56
+ def primary_key
57
+ @primary_key ||= @options[:primary_key] ||
58
+ config.settings['primary_key'] || model.primary_key || :id
59
+ end
60
+
50
61
  def render
51
62
  pre_render
52
63
  set_path
@@ -25,8 +25,12 @@ class ThinkingSphinx::Deletion
25
25
  end
26
26
 
27
27
  def execute(statement)
28
- ThinkingSphinx::Connection.take do |connection|
29
- connection.execute statement
28
+ statement = statement.gsub(/\s*\n\s*/, ' ').strip
29
+
30
+ ThinkingSphinx::Logger.log :query, statement do
31
+ ThinkingSphinx::Connection.take do |connection|
32
+ connection.execute statement
33
+ end
30
34
  end
31
35
  end
32
36
 
@@ -13,7 +13,7 @@ class ThinkingSphinx::Deltas::DefaultDelta
13
13
 
14
14
  def delete(index, instance)
15
15
  ThinkingSphinx::Deltas::DeleteJob.new(
16
- index.name, index.document_id_for_key(instance.id)
16
+ index.name, index.document_id_for_instance(instance)
17
17
  ).perform
18
18
  end
19
19
 
@@ -4,12 +4,22 @@ class ThinkingSphinx::Deltas::DeleteJob
4
4
  end
5
5
 
6
6
  def perform
7
- ThinkingSphinx::Connection.take do |connection|
8
- connection.execute Riddle::Query.update(
9
- @index_name, @document_id, :sphinx_deleted => true
10
- )
7
+ return if @document_id.nil?
8
+
9
+ ThinkingSphinx::Logger.log :query, statement do
10
+ ThinkingSphinx::Connection.take do |connection|
11
+ connection.execute statement
12
+ end
11
13
  end
12
14
  rescue ThinkingSphinx::ConnectionError => error
13
15
  # This isn't vital, so don't raise the error.
14
16
  end
17
+
18
+ private
19
+
20
+ def statement
21
+ @statement ||= Riddle::Query.update(
22
+ @index_name, @document_id, :sphinx_deleted => true
23
+ )
24
+ end
15
25
  end
@@ -21,4 +21,14 @@ class ThinkingSphinx::Distributed::Index <
21
21
  def model
22
22
  @model ||= reference.to_s.camelize.constantize
23
23
  end
24
+
25
+ def primary_key
26
+ @primary_key ||= configuration.settings['primary_key'] || :id
27
+ end
28
+
29
+ private
30
+
31
+ def configuration
32
+ ThinkingSphinx::Configuration.instance
33
+ end
24
34
  end
@@ -3,7 +3,7 @@ class ThinkingSphinx::SphinxError < StandardError
3
3
 
4
4
  def self.new_from_mysql(error)
5
5
  case error.message
6
- when /parse error/
6
+ when /parse error/, /query is non-computable/
7
7
  replacement = ThinkingSphinx::ParseError.new(error.message)
8
8
  when /syntax error/
9
9
  replacement = ThinkingSphinx::SyntaxError.new(error.message)
@@ -1,6 +1,6 @@
1
1
  class ThinkingSphinx::IndexSet
2
2
  include Enumerable
3
-
3
+
4
4
  def self.reference_name(klass)
5
5
  @cached_results ||= {}
6
6
  @cached_results[klass.name] ||= klass.name.underscore.to_sym
@@ -40,7 +40,7 @@ class ThinkingSphinx::IndexSet
40
40
  end
41
41
 
42
42
  def classes_and_ancestors
43
- @classes_and_ancestors ||= classes.collect { |model|
43
+ @classes_and_ancestors ||= mti_classes + sti_classes.collect { |model|
44
44
  model.ancestors.take_while { |klass|
45
45
  klass != ActiveRecord::Base
46
46
  }.select { |klass|
@@ -66,6 +66,12 @@ class ThinkingSphinx::IndexSet
66
66
  all_indices.select { |index| references.include? index.reference }
67
67
  end
68
68
 
69
+ def mti_classes
70
+ classes.reject { |klass|
71
+ klass.column_names.include?(klass.inheritance_column)
72
+ }
73
+ end
74
+
69
75
  def references
70
76
  options[:references] || classes_and_ancestors.collect { |klass|
71
77
  ThinkingSphinx::IndexSet.reference_name(klass)
@@ -75,4 +81,10 @@ class ThinkingSphinx::IndexSet
75
81
  def references_specified?
76
82
  options[:references] && options[:references].any?
77
83
  end
84
+
85
+ def sti_classes
86
+ classes.select { |klass|
87
+ klass.column_names.include?(klass.inheritance_column)
88
+ }
89
+ end
78
90
  end
@@ -0,0 +1,32 @@
1
+ class ThinkingSphinx::Interfaces::Daemon
2
+ include ThinkingSphinx::WithOutput
3
+
4
+ def start
5
+ if running?
6
+ raise ThinkingSphinx::SphinxAlreadyRunning, 'searchd is already running'
7
+ end
8
+
9
+ if options[:nodetach]
10
+ ThinkingSphinx::Commands::StartAttached.call configuration, options
11
+ else
12
+ ThinkingSphinx::Commands::StartDetached.call configuration, options
13
+ end
14
+ end
15
+
16
+ def status
17
+ if running?
18
+ stream.puts "The Sphinx daemon searchd is currently running."
19
+ else
20
+ stream.puts "The Sphinx daemon searchd is not currently running."
21
+ end
22
+ end
23
+
24
+ def stop
25
+ ThinkingSphinx::Commands::Stop.call configuration, options
26
+ end
27
+
28
+ private
29
+
30
+ delegate :controller, :to => :configuration
31
+ delegate :running?, :to => :controller
32
+ end
@@ -0,0 +1,41 @@
1
+ class ThinkingSphinx::Interfaces::RealTime
2
+ include ThinkingSphinx::WithOutput
3
+
4
+ def initialize(configuration, options, stream = STDOUT)
5
+ super
6
+
7
+ configuration.preload_indices
8
+
9
+ FileUtils.mkdir_p configuration.indices_location
10
+ end
11
+
12
+ def clear
13
+ indices.each do |index|
14
+ index.render
15
+ Dir["#{index.path}.*"].each { |path| FileUtils.rm path }
16
+ end
17
+
18
+ path = configuration.searchd.binlog_path
19
+ FileUtils.rm_r(path) if File.exists?(path)
20
+ end
21
+
22
+ def index
23
+ return if indices.empty? || !configuration.controller.running?
24
+
25
+ indices.each { |index| ThinkingSphinx::RealTime::Populator.populate index }
26
+ end
27
+
28
+ private
29
+
30
+ def indices
31
+ @indices ||= begin
32
+ indices = configuration.indices.select { |index| index.type == 'rt' }
33
+
34
+ if options[:index_filter]
35
+ indices.select! { |index| index.name == options[:index_filter] }
36
+ end
37
+
38
+ indices
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ class ThinkingSphinx::Interfaces::SQL
2
+ include ThinkingSphinx::WithOutput
3
+
4
+ def initialize(configuration, options, stream = STDOUT)
5
+ super
6
+
7
+ configuration.preload_indices
8
+
9
+ FileUtils.mkdir_p configuration.indices_location
10
+ end
11
+
12
+ def clear
13
+ indices.each do |index|
14
+ index.render
15
+ Dir["#{index.path}.*"].each { |path| FileUtils.rm path }
16
+ end
17
+ end
18
+
19
+ def index(reconfigure = true, verbose = nil)
20
+ stream.puts <<-TXT unless verbose.nil?
21
+ The verbose argument to the index method is now deprecated, and can instead be
22
+ managed by the :verbose option passed in when initialising RakeInterface. That
23
+ option is set automatically when invoked by rake, via rake's --silent and/or
24
+ --quiet arguments.
25
+ TXT
26
+ return if indices.empty?
27
+
28
+ ThinkingSphinx::Commands::Configure.call configuration, options if reconfigure
29
+ ThinkingSphinx.before_index_hooks.each { |hook| hook.call }
30
+
31
+ ThinkingSphinx::Commands::Index.call configuration, options, stream
32
+ end
33
+
34
+ private
35
+
36
+ def indices
37
+ @indices ||= configuration.indices.select do |index|
38
+ index.type == 'plain' || index.type.blank?
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,9 @@
1
1
  module ThinkingSphinx::Middlewares; end
2
2
 
3
- %w[middleware active_record_translator geographer glazier ids_only inquirer
4
- sphinxql stale_id_checker stale_id_filter utf8].each do |middleware|
3
+ %w[
4
+ middleware active_record_translator attribute_typer geographer glazier
5
+ ids_only inquirer sphinxql stale_id_checker stale_id_filter utf8 valid_options
6
+ ].each do |middleware|
5
7
  require "thinking_sphinx/middlewares/#{middleware}"
6
8
  end
7
9
 
@@ -10,7 +12,7 @@ module ThinkingSphinx::Middlewares
10
12
  middlewares.each { |m| builder.use m }
11
13
  end
12
14
 
13
- BASE_MIDDLEWARES = [SphinxQL, Geographer, Inquirer]
15
+ BASE_MIDDLEWARES = [ValidOptions, AttributeTyper, SphinxQL, Geographer, Inquirer]
14
16
 
15
17
  DEFAULT = ::Middleware::Builder.new do
16
18
  use StaleIdFilter
@@ -2,6 +2,7 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
2
2
  ThinkingSphinx::Middlewares::Middleware
3
3
 
4
4
  NO_MODEL = Struct.new(:primary_key).new(:id).freeze
5
+ NO_INDEX = Struct.new(:primary_key).new(:id).freeze
5
6
 
6
7
  def call(contexts)
7
8
  contexts.each do |context|
@@ -38,20 +39,23 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
38
39
  }.compact
39
40
  end
40
41
 
42
+ def index_for(model)
43
+ return NO_INDEX unless context[:indices]
44
+
45
+ context[:indices].detect { |index| index.model == model } || NO_INDEX
46
+ end
47
+
41
48
  def model_names
42
49
  @model_names ||= context[:results].collect { |row|
43
50
  row['sphinx_internal_class']
44
51
  }.uniq
45
52
  end
46
53
 
47
- def primary_key
48
- @primary_key ||= primary_key_for NO_MODEL
49
- end
50
-
51
54
  def primary_key_for(model)
52
55
  model = NO_MODEL unless model.respond_to?(:primary_key)
53
56
 
54
- context.configuration.settings['primary_key'] || model.primary_key || :id
57
+ @primary_keys ||= {}
58
+ @primary_keys[model] ||= index_for(model).primary_key
55
59
  end
56
60
 
57
61
  def reset_memos
@@ -61,13 +65,16 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
61
65
 
62
66
  def result_for(row)
63
67
  results_for_models[row['sphinx_internal_class']].detect { |record|
64
- record.public_send(primary_key) == row['sphinx_internal_id']
68
+ record.public_send(
69
+ primary_key_for(record.class)
70
+ ) == row['sphinx_internal_id']
65
71
  }
66
72
  end
67
73
 
68
74
  def results_for_models
69
75
  @results_for_models ||= model_names.inject({}) do |hash, name|
70
76
  model = name.constantize
77
+
71
78
  hash[name] = model_relation_with_sql_options(model.unscoped).where(
72
79
  primary_key_for(model) => ids_for_model(name)
73
80
  )
@@ -0,0 +1,48 @@
1
+ class ThinkingSphinx::Middlewares::AttributeTyper <
2
+ ThinkingSphinx::Middlewares::Middleware
3
+
4
+ def call(contexts)
5
+ contexts.each do |context|
6
+ deprecate_filters_in context.search.options[:with]
7
+ deprecate_filters_in context.search.options[:without]
8
+ deprecate_filters_in context.search.options[:with_all]
9
+ deprecate_filters_in context.search.options[:without_all]
10
+ end
11
+
12
+ app.call contexts
13
+ end
14
+
15
+ private
16
+
17
+ def attributes
18
+ @attributes ||= ThinkingSphinx::AttributeTypes.call
19
+ end
20
+
21
+ def casted_value_for(type, value)
22
+ case type
23
+ when :uint, :bigint, :timestamp, :bool
24
+ value.to_i
25
+ when :float
26
+ value.to_f
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ def deprecate_filters_in(filters)
33
+ return if filters.nil?
34
+
35
+ filters.each do |key, value|
36
+ known_types = attributes[key.to_s] || [:string]
37
+
38
+ next unless value.is_a?(String) && !known_types.include?(:string)
39
+
40
+ ActiveSupport::Deprecation.warn(<<-MSG.squish, caller(11))
41
+ You are filtering on a non-string attribute #{key} with a string value (#{value.inspect}).
42
+ Thinking Sphinx will quote string values by default in upcoming releases (which will cause query syntax errors on non-string attributes), so please cast these values to their appropriate types.
43
+ MSG
44
+
45
+ filters[key] = casted_value_for known_types.first, value
46
+ end
47
+ end
48
+ end