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,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