thinking-sphinx 3.0.0 → 3.0.1

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 (52) hide show
  1. data/.travis.yml +6 -3
  2. data/HISTORY +18 -0
  3. data/README.textile +31 -8
  4. data/gemfiles/rails_3_1.gemfile +2 -2
  5. data/gemfiles/rails_3_2.gemfile +2 -2
  6. data/lib/thinking/sphinx.rb +1 -0
  7. data/lib/thinking_sphinx.rb +1 -0
  8. data/lib/thinking_sphinx/active_record.rb +2 -0
  9. data/lib/thinking_sphinx/active_record/association.rb +8 -0
  10. data/lib/thinking_sphinx/active_record/associations.rb +25 -5
  11. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +1 -5
  12. data/lib/thinking_sphinx/active_record/column.rb +12 -0
  13. data/lib/thinking_sphinx/active_record/database_adapters.rb +7 -0
  14. data/lib/thinking_sphinx/active_record/filtered_reflection.rb +49 -0
  15. data/lib/thinking_sphinx/active_record/interpreter.rb +6 -0
  16. data/lib/thinking_sphinx/active_record/polymorpher.rb +50 -0
  17. data/lib/thinking_sphinx/active_record/property.rb +6 -0
  18. data/lib/thinking_sphinx/active_record/property_query.rb +1 -1
  19. data/lib/thinking_sphinx/active_record/property_sql_presenter.rb +7 -1
  20. data/lib/thinking_sphinx/active_record/sql_builder.rb +7 -2
  21. data/lib/thinking_sphinx/active_record/sql_source.rb +5 -1
  22. data/lib/thinking_sphinx/capistrano.rb +64 -0
  23. data/lib/thinking_sphinx/configuration.rb +10 -1
  24. data/lib/thinking_sphinx/connection.rb +20 -0
  25. data/lib/thinking_sphinx/errors.rb +24 -0
  26. data/lib/thinking_sphinx/middlewares/sphinxql.rb +4 -1
  27. data/lib/thinking_sphinx/real_time/transcriber.rb +1 -1
  28. data/lib/thinking_sphinx/search/batch_inquirer.rb +3 -1
  29. data/lib/thinking_sphinx/search/glaze.rb +3 -3
  30. data/spec/acceptance/index_options_spec.rb +5 -0
  31. data/spec/acceptance/searching_on_fields_spec.rb +1 -0
  32. data/spec/acceptance/specifying_sql_spec.rb +107 -0
  33. data/spec/acceptance/sql_deltas_spec.rb +9 -0
  34. data/spec/acceptance/support/sphinx_controller.rb +1 -0
  35. data/spec/internal/app/models/event.rb +3 -0
  36. data/spec/internal/app/models/hardcover.rb +3 -0
  37. data/spec/internal/db/schema.rb +7 -1
  38. data/spec/thinking_sphinx/active_record/associations_spec.rb +2 -1
  39. data/spec/thinking_sphinx/active_record/callbacks/delta_callbacks_spec.rb +3 -3
  40. data/spec/thinking_sphinx/active_record/column_spec.rb +23 -0
  41. data/spec/thinking_sphinx/active_record/database_adapters_spec.rb +18 -0
  42. data/spec/thinking_sphinx/active_record/filtered_reflection_spec.rb +141 -0
  43. data/spec/thinking_sphinx/active_record/polymorpher_spec.rb +65 -0
  44. data/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb +28 -4
  45. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +11 -1
  46. data/spec/thinking_sphinx/configuration_spec.rb +24 -0
  47. data/spec/thinking_sphinx/connection_spec.rb +82 -0
  48. data/spec/thinking_sphinx/errors_spec.rb +36 -0
  49. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +25 -0
  50. data/spec/thinking_sphinx/search/glaze_spec.rb +3 -0
  51. data/thinking-sphinx.gemspec +1 -1
  52. metadata +25 -3
@@ -12,8 +12,9 @@ class ThinkingSphinx::ActiveRecord::SQLBuilder
12
12
  relation = relation.group group_clause
13
13
  relation = relation.order('NULL') if source.type == 'mysql'
14
14
  relation = relation.joins associations.join_values
15
+ relation = relation.joins custom_joins.collect(&:to_s) if custom_joins.any?
15
16
 
16
- relation.to_sql
17
+ relation.to_sql.gsub(/\n/, "\\\n")
17
18
  end
18
19
 
19
20
  def sql_query_range
@@ -67,12 +68,16 @@ class ThinkingSphinx::ActiveRecord::SQLBuilder
67
68
 
68
69
  def associations
69
70
  @associations ||= ThinkingSphinx::ActiveRecord::Associations.new(model).tap do |assocs|
70
- source.associations.each do |association|
71
+ source.associations.reject(&:string?).each do |association|
71
72
  assocs.add_join_to association.stack
72
73
  end
73
74
  end
74
75
  end
75
76
 
77
+ def custom_joins
78
+ @custom_joins ||= source.associations.select &:string?
79
+ end
80
+
76
81
  def quote_column(column)
77
82
  model.connection.quote_column_name(column)
78
83
  end
@@ -1,6 +1,7 @@
1
1
  class ThinkingSphinx::ActiveRecord::SQLSource < Riddle::Configuration::SQLSource
2
2
  attr_reader :model, :database_settings, :options
3
- attr_accessor :fields, :attributes, :associations, :conditions, :groupings
3
+ attr_accessor :fields, :attributes, :associations, :conditions, :groupings,
4
+ :polymorphs
4
5
 
5
6
  OPTIONS = [:name, :offset, :delta_processor, :delta?, :disable_range?,
6
7
  :group_concat_max_len, :utf8?, :position]
@@ -15,6 +16,7 @@ class ThinkingSphinx::ActiveRecord::SQLSource < Riddle::Configuration::SQLSource
15
16
  @associations = []
16
17
  @conditions = []
17
18
  @groupings = []
19
+ @polymorphs = []
18
20
 
19
21
  Template.new(self).apply
20
22
 
@@ -89,6 +91,8 @@ class ThinkingSphinx::ActiveRecord::SQLSource < Riddle::Configuration::SQLSource
89
91
  end
90
92
 
91
93
  def prepare_for_render
94
+ polymorphs.each &:morph!
95
+
92
96
  set_database_settings
93
97
 
94
98
  fields.each do |field|
@@ -0,0 +1,64 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+ namespace :thinking_sphinx do
3
+ desc 'Generate the Sphinx configuration file.'
4
+ task :configure do
5
+ rake 'thinking_sphinx:configure'
6
+ end
7
+
8
+ desc 'Build Sphinx indexes into the shared path and symlink them into your release.'
9
+ task :index do
10
+ rake 'thinking_sphinx:index'
11
+ end
12
+ after 'thinking_sphinx:index', 'thinking_sphinx:symlink_indexes'
13
+
14
+ desc 'Start the Sphinx search daemon.'
15
+ task :start do
16
+ rake 'thinking_sphinx:start'
17
+ end
18
+ before 'thinking_sphinx:start', 'thinking_sphinx:configure'
19
+
20
+ desc 'Stop the Sphinx search daemon.'
21
+ task :stop do
22
+ rake 'thinking_sphinx:stop'
23
+ end
24
+
25
+ desc 'Restart the Sphinx search daemon.'
26
+ task :restart do
27
+ rake 'thinking_sphinx:stop thinking_sphinx:configure thinking_sphinx:start'
28
+ end
29
+
30
+ desc <<-DESC
31
+ Stop, reindex, and then start the Sphinx search daemon. This task must be executed \
32
+ if you alter the structure of your indexes.
33
+ DESC
34
+ task :rebuild do
35
+ rake 'thinking_sphinx:stop thinking_sphinx:reindex'
36
+ end
37
+ after 'thinking_sphinx:rebuild', 'thinking_sphinx:symlink_indexes'
38
+ after 'thinking_sphinx:rebuild', 'thinking_sphinx:start'
39
+
40
+ desc 'Create the shared folder for sphinx indexes.'
41
+ task :shared_sphinx_folder do
42
+ rails_env = fetch(:rails_env, 'production')
43
+ run "mkdir -p #{shared_path}/db/sphinx/#{rails_env}"
44
+ end
45
+
46
+ desc 'Symlink Sphinx indexes from the shared folder to the latest release.'
47
+ task :symlink_indexes do
48
+ run "if [ -d #{release_path} ]; then ln -nfs #{shared_path}/db/sphinx #{release_path}/db/sphinx; else ln -nfs #{shared_path}/db/sphinx #{current_path}/db/sphinx; fi;"
49
+ end
50
+
51
+ # Logical flow for deploying an app
52
+ after 'deploy:cold', 'thinking_sphinx:index'
53
+ after 'deploy:cold', 'thinking_sphinx:start'
54
+ after 'deploy:setup', 'thinking_sphinx:shared_sphinx_folder'
55
+ after 'deploy:finalize_update', 'thinking_sphinx:symlink_indexes'
56
+
57
+ def rake(tasks)
58
+ rails_env = fetch(:rails_env, 'production')
59
+ rake = fetch(:rake, 'rake')
60
+
61
+ run "if [ -d #{release_path} ]; then cd #{release_path}; else cd #{current_path}; fi; if [ -f Rakefile ]; then #{rake} RAILS_ENV=#{rails_env} #{tasks}; fi;"
62
+ end
63
+ end
64
+ end
@@ -1,5 +1,5 @@
1
1
  class ThinkingSphinx::Configuration < Riddle::Configuration
2
- attr_accessor :configuration_file, :indices_location
2
+ attr_accessor :configuration_file, :indices_location, :version
3
3
  attr_reader :index_paths
4
4
  attr_writer :controller, :framework
5
5
 
@@ -11,6 +11,7 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
11
11
  @index_paths = [File.join(framework.root, 'app', 'indices')]
12
12
  @indices_location = File.join framework.root, 'db', 'sphinx',
13
13
  framework.environment
14
+ @version = settings['version'] || '2.0.6'
14
15
 
15
16
  searchd.pid_file = File.join framework.root, 'log',
16
17
  "#{framework.environment}.sphinx.pid"
@@ -27,6 +28,14 @@ class ThinkingSphinx::Configuration < Riddle::Configuration
27
28
  Defaults::PORT
28
29
  searchd.workers = 'threads'
29
30
 
31
+ [indexer, searchd].each do |object|
32
+ settings.each do |key, value|
33
+ next unless object.class.settings.include?(key.to_sym)
34
+
35
+ object.send("#{key}=", value)
36
+ end
37
+ end
38
+
30
39
  @offsets = {}
31
40
  end
32
41
 
@@ -28,6 +28,26 @@ module ThinkingSphinx::Connection
28
28
  )
29
29
  end
30
30
 
31
+ def self.take
32
+ retries = 0
33
+ original = nil
34
+ begin
35
+ pool.take do |connection|
36
+ begin
37
+ yield connection
38
+ rescue Mysql2::Error => error
39
+ original = ThinkingSphinx::SphinxError.new_from_mysql error
40
+ raise original if original.is_a?(ThinkingSphinx::QueryError)
41
+ raise Innertube::Pool::BadResource
42
+ end
43
+ end
44
+ rescue Innertube::Pool::BadResource
45
+ retries += 1
46
+ retry if retries < 3
47
+ raise original
48
+ end
49
+ end
50
+
31
51
  class MRI
32
52
  attr_reader :client
33
53
 
@@ -0,0 +1,24 @@
1
+ class ThinkingSphinx::SphinxError < StandardError
2
+ def self.new_from_mysql(error)
3
+ case error.message
4
+ when /parse error/
5
+ replacement = ThinkingSphinx::ParseError.new(error.message)
6
+ when /syntax error/
7
+ replacement = ThinkingSphinx::SyntaxError.new(error.message)
8
+ else
9
+ replacement = new(error.message)
10
+ end
11
+
12
+ replacement.set_backtrace error.backtrace
13
+ replacement
14
+ end
15
+ end
16
+
17
+ class ThinkingSphinx::QueryError < ThinkingSphinx::SphinxError
18
+ end
19
+
20
+ class ThinkingSphinx::SyntaxError < ThinkingSphinx::QueryError
21
+ end
22
+
23
+ class ThinkingSphinx::ParseError < ThinkingSphinx::QueryError
24
+ end
@@ -40,7 +40,10 @@ class ThinkingSphinx::Middlewares::SphinxQL <
40
40
  end
41
41
 
42
42
  def class_condition
43
- '(' + classes_and_descendants.collect(&:name).join('|') + ')'
43
+ class_names = classes_and_descendants.collect(&:name).collect { |name|
44
+ name[/:/] ? "\"#{name}\"" : name
45
+ }
46
+ '(' + class_names.join('|') + ')'
44
47
  end
45
48
 
46
49
  def descendants
@@ -13,7 +13,7 @@ class ThinkingSphinx::RealTime::Transcriber
13
13
  end
14
14
 
15
15
  sphinxql = Riddle::Query::Insert.new index.name, columns, values
16
- ThinkingSphinx::Connection.pool.take do |connection|
16
+ ThinkingSphinx::Connection.take do |connection|
17
17
  connection.execute sphinxql.replace!.to_sql
18
18
  end
19
19
  end
@@ -13,7 +13,9 @@ class ThinkingSphinx::Search::BatchInquirer
13
13
  @results ||= begin
14
14
  @queries.freeze
15
15
 
16
- ThinkingSphinx::Connection.new.query_all *@queries
16
+ ThinkingSphinx::Connection.take do |connection|
17
+ connection.query_all *@queries
18
+ end
17
19
  end
18
20
  end
19
21
  end
@@ -22,11 +22,11 @@ class ThinkingSphinx::Search::Glaze < BasicObject
22
22
  private
23
23
 
24
24
  def method_missing(method, *args, &block)
25
- if @object.respond_to?(method)
25
+ pane = @panes.detect { |pane| pane.respond_to?(method) }
26
+ if @object.respond_to?(method) || pane.nil?
26
27
  @object.send(method, *args, &block)
27
28
  else
28
- pane = @panes.detect { |pane| pane.respond_to?(method) }
29
- pane.nil? ? super : pane.send(method, *args, &block)
29
+ pane.send(method, *args, &block)
30
30
  end
31
31
  end
32
32
  end
@@ -92,6 +92,7 @@ describe 'Index options' do
92
92
 
93
93
  set_property :sql_range_step => 5
94
94
  set_property :disable_range? => true
95
+ set_property :sql_query_pre => ["DO STUFF"]
95
96
  }
96
97
  index.render
97
98
  end
@@ -103,5 +104,9 @@ describe 'Index options' do
103
104
  it "allows for source options" do
104
105
  index.sources.first.disable_range?.should be_true
105
106
  end
107
+
108
+ it "respects sql_query_pre values" do
109
+ index.sources.first.sql_query_pre.should == ["DO STUFF"]
110
+ end
106
111
  end
107
112
  end
@@ -50,6 +50,7 @@ describe 'Searching on fields', :live => true do
50
50
  File.open(file_path, 'w') { |file| file.print 'Cyberpunk at its best' }
51
51
 
52
52
  book = Book.create! :title => 'Accelerando', :blurb_file => file_path.to_s
53
+ index
53
54
 
54
55
  Book.search('cyberpunk').to_a.should == [book]
55
56
  end
@@ -36,6 +36,18 @@ describe 'specifying SQL for index definitions' do
36
36
  query.should match(/LEFT OUTER JOIN .tags./)
37
37
  end
38
38
 
39
+ it "handles custom join SQL statements" do
40
+ index = ThinkingSphinx::ActiveRecord::Index.new(:article)
41
+ index.definition_block = Proc.new {
42
+ indexes title
43
+ join "INNER JOIN foo ON foo.x = bar.y"
44
+ }
45
+ index.render
46
+
47
+ query = index.sources.first.sql_query
48
+ query.should match(/INNER JOIN foo ON foo.x = bar.y/)
49
+ end
50
+
39
51
  it "handles GROUP BY clauses" do
40
52
  index = ThinkingSphinx::ActiveRecord::Index.new(:article)
41
53
  index.definition_block = Proc.new {
@@ -83,6 +95,66 @@ describe 'specifying SQL for index definitions' do
83
95
  query = index.sources.first.sql_query
84
96
  query.should match(/WHERE .+title != 'secret'.+ GROUP BY/)
85
97
  end
98
+
99
+ it "escapes new lines in SQL snippets" do
100
+ index = ThinkingSphinx::ActiveRecord::Index.new(:article)
101
+ index.definition_block = Proc.new {
102
+ indexes title
103
+ has <<-SQL, as: :custom_attribute, type: :integer
104
+ ARRAY_AGG(
105
+ CONCAT(
106
+ something
107
+ )
108
+ )
109
+ SQL
110
+ }
111
+ index.render
112
+
113
+ query = index.sources.first.sql_query
114
+ query.should match(/\\\n/)
115
+ end
116
+
117
+ it "joins each polymorphic relation" do
118
+ index = ThinkingSphinx::ActiveRecord::Index.new(:event)
119
+ index.definition_block = Proc.new {
120
+ indexes eventable.title, :as => :title
121
+ polymorphs eventable, :to => %w(Article Book)
122
+ }
123
+ index.render
124
+
125
+ query = index.sources.first.sql_query
126
+ query.should match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/)
127
+ query.should match(/LEFT OUTER JOIN .books. ON .books.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Book'/)
128
+ query.should match(/articles\..title., books\..title./)
129
+ end
130
+
131
+ it "concatenates references where that have column" do
132
+ index = ThinkingSphinx::ActiveRecord::Index.new(:event)
133
+ index.definition_block = Proc.new {
134
+ indexes eventable.title, :as => :title
135
+ polymorphs eventable, :to => %w(Article User)
136
+ }
137
+ index.render
138
+
139
+ query = index.sources.first.sql_query
140
+ query.should match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/)
141
+ query.should match(/LEFT OUTER JOIN .users. ON .users.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'User'/)
142
+ query.should_not match(/articles\..title., users\..title./)
143
+ end
144
+
145
+ it "respects deeper associations through polymorphic joins" do
146
+ index = ThinkingSphinx::ActiveRecord::Index.new(:event)
147
+ index.definition_block = Proc.new {
148
+ indexes eventable.user.name, :as => :user_name
149
+ polymorphs eventable, :to => %w(Article Book)
150
+ }
151
+ index.render
152
+
153
+ query = index.sources.first.sql_query
154
+ query.should match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/)
155
+ query.should match(/LEFT OUTER JOIN .users. ON .users.\..id. = .articles.\..user_id./)
156
+ query.should match(/users\..name./)
157
+ end
86
158
  end
87
159
 
88
160
  describe 'separate queries for MVAs' do
@@ -247,6 +319,25 @@ describe 'separate queries for MVAs' do
247
319
  query.should == 'My Custom SQL Query'
248
320
  range.should == 'And a Range'
249
321
  end
322
+
323
+ it "escapes new lines in custom SQL snippets" do
324
+ index.definition_block = Proc.new {
325
+ indexes title
326
+ has <<-SQL, :as => :tag_ids, :source => :query, :type => :integer, :multi => true
327
+ My Custom
328
+ SQL Query
329
+ SQL
330
+ }
331
+ index.render
332
+
333
+ attribute = source.sql_attr_multi.detect { |attribute|
334
+ attribute[/tag_ids/]
335
+ }
336
+ declaration, query = attribute.split(/;\s+/)
337
+
338
+ declaration.should == 'uint tag_ids from query'
339
+ query.should == "My Custom\\\nSQL Query"
340
+ end
250
341
  end
251
342
 
252
343
  describe 'separate queries for field' do
@@ -336,4 +427,20 @@ describe 'separate queries for field' do
336
427
  query.should == 'My Custom SQL Query'
337
428
  range.should == 'And a Range'
338
429
  end
430
+
431
+ it "escapes new lines in custom SQL snippets" do
432
+ index.definition_block = Proc.new {
433
+ indexes <<-SQL, :as => :tags, :source => :query
434
+ My Custom
435
+ SQL Query
436
+ SQL
437
+ }
438
+ index.render
439
+
440
+ field = source.sql_joined_field.detect { |field| field[/tags/] }
441
+ declaration, query = field.split(/;\s+/)
442
+
443
+ declaration.should == 'tags from query'
444
+ query.should == "My Custom\\\nSQL Query"
445
+ end
339
446
  end
@@ -40,4 +40,13 @@ describe 'SQL delta indexing', :live => true do
40
40
 
41
41
  Book.search('Harry').should be_empty
42
42
  end
43
+
44
+ it "automatically indexes new records of subclasses" do
45
+ book = Hardcover.create(
46
+ :title => 'American Gods', :author => 'Neil Gaiman'
47
+ )
48
+ sleep 0.25
49
+
50
+ Book.search('Gaiman').to_a.should == [book]
51
+ end
43
52
  end
@@ -18,6 +18,7 @@ class SphinxController
18
18
  config.searchd.mysql41 = 9307
19
19
  config.settings['quiet_deltas'] = true
20
20
  config.settings['attribute_updates'] = true
21
+ config.controller.bin_path = ENV['SPHINX_BIN'] || ''
21
22
  end
22
23
 
23
24
  def start
@@ -0,0 +1,3 @@
1
+ class Event < ActiveRecord::Base
2
+ belongs_to :eventable, :polymorphic => true
3
+ end
@@ -0,0 +1,3 @@
1
+ class Hardcover < Book
2
+ #
3
+ end
@@ -22,7 +22,8 @@ ActiveRecord::Schema.define do
22
22
  t.string :author
23
23
  t.integer :year
24
24
  t.string :blurb_file
25
- t.boolean :delta, :default => true, :null => false
25
+ t.boolean :delta, :default => true, :null => false
26
+ t.string :type, :default => 'Book', :null => false
26
27
  t.timestamps
27
28
  end
28
29
 
@@ -42,6 +43,11 @@ ActiveRecord::Schema.define do
42
43
  t.timestamps
43
44
  end
44
45
 
46
+ create_table(:events, :force => true) do |t|
47
+ t.string :eventable_type
48
+ t.integer :eventable_id
49
+ end
50
+
45
51
  create_table(:genres, :force => true) do |t|
46
52
  t.string :name
47
53
  end