thinking-sphinx 3.1.1 → 3.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -6
  3. data/HISTORY +21 -0
  4. data/lib/thinking_sphinx.rb +1 -0
  5. data/lib/thinking_sphinx/active_record.rb +1 -0
  6. data/lib/thinking_sphinx/active_record/base.rb +3 -2
  7. data/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +4 -0
  8. data/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb +4 -0
  9. data/lib/thinking_sphinx/active_record/filtered_reflection.rb +10 -2
  10. data/lib/thinking_sphinx/active_record/interpreter.rb +2 -1
  11. data/lib/thinking_sphinx/active_record/join_association.rb +13 -0
  12. data/lib/thinking_sphinx/active_record/log_subscriber.rb +8 -3
  13. data/lib/thinking_sphinx/active_record/polymorpher.rb +8 -1
  14. data/lib/thinking_sphinx/active_record/sql_builder.rb +19 -4
  15. data/lib/thinking_sphinx/active_record/sql_builder/statement.rb +19 -12
  16. data/lib/thinking_sphinx/active_record/sql_source.rb +2 -2
  17. data/lib/thinking_sphinx/capistrano/v3.rb +1 -1
  18. data/lib/thinking_sphinx/configuration.rb +1 -1
  19. data/lib/thinking_sphinx/connection.rb +4 -2
  20. data/lib/thinking_sphinx/controller.rb +3 -13
  21. data/lib/thinking_sphinx/core/index.rb +13 -3
  22. data/lib/thinking_sphinx/core/interpreter.rb +4 -0
  23. data/lib/thinking_sphinx/deletion.rb +5 -3
  24. data/lib/thinking_sphinx/errors.rb +3 -0
  25. data/lib/thinking_sphinx/facet.rb +3 -2
  26. data/lib/thinking_sphinx/facet_search.rb +6 -2
  27. data/lib/thinking_sphinx/guard.rb +6 -0
  28. data/lib/thinking_sphinx/guard/file.rb +26 -0
  29. data/lib/thinking_sphinx/guard/files.rb +38 -0
  30. data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -4
  31. data/lib/thinking_sphinx/masks/weight_enumerator_mask.rb +1 -1
  32. data/lib/thinking_sphinx/middlewares/sphinxql.rb +2 -2
  33. data/lib/thinking_sphinx/panes/weight_pane.rb +1 -1
  34. data/lib/thinking_sphinx/rake_interface.rb +20 -1
  35. data/lib/thinking_sphinx/real_time.rb +2 -2
  36. data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +10 -4
  37. data/lib/thinking_sphinx/real_time/interpreter.rb +2 -1
  38. data/lib/thinking_sphinx/real_time/transcriber.rb +1 -1
  39. data/lib/thinking_sphinx/search.rb +4 -0
  40. data/lib/thinking_sphinx/search/merger.rb +4 -0
  41. data/lib/thinking_sphinx/sphinxql.rb +12 -6
  42. data/lib/thinking_sphinx/tasks.rb +13 -3
  43. data/spec/acceptance/attribute_access_spec.rb +2 -2
  44. data/spec/acceptance/big_integers_spec.rb +11 -0
  45. data/spec/acceptance/facets_spec.rb +4 -1
  46. data/spec/acceptance/indexing_spec.rb +9 -0
  47. data/spec/acceptance/specifying_sql_spec.rb +1 -1
  48. data/spec/acceptance/sphinx_scopes_spec.rb +9 -0
  49. data/spec/internal/app/indices/user_index.rb +2 -0
  50. data/spec/thinking_sphinx/active_record/base_spec.rb +3 -1
  51. data/spec/thinking_sphinx/active_record/index_spec.rb +8 -0
  52. data/spec/thinking_sphinx/active_record/interpreter_spec.rb +7 -1
  53. data/spec/thinking_sphinx/active_record/polymorpher_spec.rb +16 -3
  54. data/spec/thinking_sphinx/configuration_spec.rb +18 -0
  55. data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
  56. data/spec/thinking_sphinx/masks/scopes_mask_spec.rb +6 -1
  57. data/spec/thinking_sphinx/panes/weight_pane_spec.rb +1 -1
  58. data/spec/thinking_sphinx/rake_interface_spec.rb +62 -3
  59. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +38 -4
  60. data/spec/thinking_sphinx/real_time/index_spec.rb +4 -0
  61. data/spec/thinking_sphinx/real_time/interpreter_spec.rb +7 -1
  62. data/thinking-sphinx.gemspec +1 -1
  63. metadata +7 -3
@@ -13,6 +13,10 @@ class ThinkingSphinx::Core::Interpreter < BasicObject
13
13
 
14
14
  private
15
15
 
16
+ def search_option?(key)
17
+ ::ThinkingSphinx::Middlewares::SphinxQL::SELECT_OPTIONS.include? key
18
+ end
19
+
16
20
  def method_missing(method, *args)
17
21
  ::ThinkingSphinx::ActiveRecord::Column.new method, *args
18
22
  end
@@ -38,11 +38,13 @@ class ThinkingSphinx::Deletion
38
38
 
39
39
  class PlainDeletion < ThinkingSphinx::Deletion
40
40
  def perform
41
- execute <<-SQL
41
+ document_ids_for_keys.each_slice(1000) do |document_ids|
42
+ execute <<-SQL
42
43
  UPDATE #{name}
43
44
  SET sphinx_deleted = 1
44
- WHERE id IN (#{document_ids_for_keys.join(', ')})
45
- SQL
45
+ WHERE id IN (#{document_ids.join(', ')})
46
+ SQL
47
+ end
46
48
  end
47
49
  end
48
50
  end
@@ -47,3 +47,6 @@ end
47
47
 
48
48
  class ThinkingSphinx::MissingColumnError < StandardError
49
49
  end
50
+
51
+ class ThinkingSphinx::PopulatedResultsError < StandardError
52
+ end
@@ -11,7 +11,7 @@ class ThinkingSphinx::Facet
11
11
 
12
12
  def results_from(raw)
13
13
  raw.inject({}) { |hash, row|
14
- hash[row[group_column]] = row['sphinx_internal_count']
14
+ hash[row[group_column]] = row[ThinkingSphinx::SphinxQL.count[:column]]
15
15
  hash
16
16
  }
17
17
  end
@@ -19,7 +19,8 @@ class ThinkingSphinx::Facet
19
19
  private
20
20
 
21
21
  def group_column
22
- @properties.any?(&:multi?) ? 'sphinx_internal_group' : name
22
+ @properties.any?(&:multi?) ?
23
+ ThinkingSphinx::SphinxQL.group_by[:column] : name
23
24
  end
24
25
 
25
26
  def use_field?
@@ -51,6 +51,10 @@ class ThinkingSphinx::FacetSearch
51
51
  @populated = true
52
52
  end
53
53
 
54
+ def populated?
55
+ @populated
56
+ end
57
+
54
58
  def to_hash
55
59
  populate
56
60
 
@@ -102,8 +106,8 @@ class ThinkingSphinx::FacetSearch
102
106
  def options_for(facet)
103
107
  options.merge(
104
108
  :select => [(options[:select] || '*'),
105
- "#{ThinkingSphinx::SphinxQL.group_by} as sphinx_internal_group",
106
- "#{ThinkingSphinx::SphinxQL.count} as sphinx_internal_count"
109
+ "#{ThinkingSphinx::SphinxQL.group_by[:select]}",
110
+ "#{ThinkingSphinx::SphinxQL.count[:select]}"
107
111
  ].join(', '),
108
112
  :group_by => facet.name,
109
113
  :indices => index_names_for(facet),
@@ -0,0 +1,6 @@
1
+ module ThinkingSphinx::Guard
2
+ #
3
+ end
4
+
5
+ require 'thinking_sphinx/guard/file'
6
+ require 'thinking_sphinx/guard/files'
@@ -0,0 +1,26 @@
1
+ class ThinkingSphinx::Guard::File
2
+ attr_reader :name
3
+
4
+ def initialize(name)
5
+ @name = name
6
+ end
7
+
8
+ def lock
9
+ FileUtils.touch path
10
+ end
11
+
12
+ def locked?
13
+ File.exists? path
14
+ end
15
+
16
+ def path
17
+ @path ||= File.join(
18
+ ThinkingSphinx::Configuration.instance.indices_location,
19
+ "ts-#{name}.tmp"
20
+ )
21
+ end
22
+
23
+ def unlock
24
+ FileUtils.rm path
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ class ThinkingSphinx::Guard::Files
2
+ def self.call(names, &block)
3
+ new(names).call(&block)
4
+ end
5
+
6
+ def initialize(names)
7
+ @names = names
8
+ end
9
+
10
+ def call(&block)
11
+ return if unlocked.empty?
12
+
13
+ unlocked.each &:lock
14
+ block.call unlocked.collect(&:name)
15
+ rescue => error
16
+ raise error
17
+ ensure
18
+ unlocked.each &:unlock
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :names
24
+
25
+ def log_lock(file)
26
+ ThinkingSphinx::Logger.log :guard,
27
+ "Guard file for index #{file.name} exists, not indexing: #{file.path}."
28
+ end
29
+
30
+ def unlocked
31
+ @unlocked ||= names.collect { |name|
32
+ ThinkingSphinx::Guard::File.new name
33
+ }.reject { |file|
34
+ log_lock file if file.locked?
35
+ file.locked?
36
+ }
37
+ end
38
+ end
@@ -9,20 +9,20 @@ class ThinkingSphinx::Masks::GroupEnumeratorsMask
9
9
 
10
10
  def each_with_count(&block)
11
11
  @search.raw.each_with_index do |row, index|
12
- yield @search[index], row['sphinx_internal_count']
12
+ yield @search[index], row[ThinkingSphinx::SphinxQL.count[:column]]
13
13
  end
14
14
  end
15
15
 
16
16
  def each_with_group(&block)
17
17
  @search.raw.each_with_index do |row, index|
18
- yield @search[index], row['sphinx_internal_group']
18
+ yield @search[index], row[ThinkingSphinx::SphinxQL.group_by[:column]]
19
19
  end
20
20
  end
21
21
 
22
22
  def each_with_group_and_count(&block)
23
23
  @search.raw.each_with_index do |row, index|
24
- yield @search[index], row['sphinx_internal_group'],
25
- row['sphinx_internal_count']
24
+ yield @search[index], row[ThinkingSphinx::SphinxQL.group_by[:column]],
25
+ row[ThinkingSphinx::SphinxQL.count[:column]]
26
26
  end
27
27
  end
28
28
  end
@@ -9,7 +9,7 @@ class ThinkingSphinx::Masks::WeightEnumeratorMask
9
9
 
10
10
  def each_with_weight(&block)
11
11
  @search.raw.each_with_index do |row, index|
12
- yield @search[index], row[ThinkingSphinx::SphinxQL.weight]
12
+ yield @search[index], row[ThinkingSphinx::SphinxQL.weight[:column]]
13
13
  end
14
14
  end
15
15
  end
@@ -157,8 +157,8 @@ SQL
157
157
 
158
158
  def values
159
159
  options[:select] ||= ['*',
160
- "#{ThinkingSphinx::SphinxQL.group_by} as sphinx_internal_group",
161
- "#{ThinkingSphinx::SphinxQL.count} as sphinx_internal_count"
160
+ "#{ThinkingSphinx::SphinxQL.group_by[:select]}",
161
+ "#{ThinkingSphinx::SphinxQL.count[:select]}"
162
162
  ].join(', ') if group_attribute.present?
163
163
  options[:select]
164
164
  end
@@ -4,6 +4,6 @@ class ThinkingSphinx::Panes::WeightPane
4
4
  end
5
5
 
6
6
  def weight
7
- @raw[ThinkingSphinx::SphinxQL.weight]
7
+ @raw[ThinkingSphinx::SphinxQL.weight[:column]]
8
8
  end
9
9
  end
@@ -1,5 +1,5 @@
1
1
  class ThinkingSphinx::RakeInterface
2
- def clear
2
+ def clear_all
3
3
  [
4
4
  configuration.indices_location,
5
5
  configuration.searchd.binlog_path
@@ -8,6 +8,17 @@ class ThinkingSphinx::RakeInterface
8
8
  end
9
9
  end
10
10
 
11
+ def clear_real_time
12
+ indices = configuration.indices.select { |index| index.type == 'rt' }
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
+
11
22
  def configure
12
23
  puts "Generating configuration to #{configuration.configuration_file}"
13
24
  configuration.render_to_file
@@ -47,6 +58,14 @@ class ThinkingSphinx::RakeInterface
47
58
  end
48
59
  end
49
60
 
61
+ def status
62
+ if controller.running?
63
+ puts "The Sphinx daemon searchd is currently running."
64
+ else
65
+ puts "The Sphinx daemon searchd is not currently running."
66
+ end
67
+ end
68
+
50
69
  def stop
51
70
  unless controller.running?
52
71
  puts 'searchd is not currently running.' and return
@@ -3,8 +3,8 @@ module ThinkingSphinx::RealTime
3
3
  #
4
4
  end
5
5
 
6
- def self.callback_for(reference, path = [])
7
- Callbacks::RealTimeCallbacks.new reference, path
6
+ def self.callback_for(reference, path = [], &block)
7
+ Callbacks::RealTimeCallbacks.new reference, path, &block
8
8
  end
9
9
  end
10
10
 
@@ -1,6 +1,6 @@
1
1
  class ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks
2
- def initialize(reference, path = [])
3
- @reference, @path = reference, path
2
+ def initialize(reference, path = [], &block)
3
+ @reference, @path, @block = reference, path, block
4
4
  end
5
5
 
6
6
  def after_save(instance)
@@ -15,7 +15,7 @@ class ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks
15
15
 
16
16
  private
17
17
 
18
- attr_reader :reference, :path
18
+ attr_reader :reference, :path, :block
19
19
 
20
20
  def callbacks_enabled?
21
21
  setting = configuration.settings['real_time_callbacks']
@@ -31,7 +31,13 @@ class ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks
31
31
  end
32
32
 
33
33
  def objects_for(instance)
34
- Array(path.inject(instance) { |object, method| object.send method })
34
+ if block
35
+ results = block.call instance
36
+ else
37
+ results = path.inject(instance) { |object, method| object.send method }
38
+ end
39
+
40
+ Array results
35
41
  end
36
42
 
37
43
  def real_time_indices?
@@ -23,7 +23,8 @@ class ThinkingSphinx::RealTime::Interpreter <
23
23
 
24
24
  def set_property(properties)
25
25
  properties.each do |key, value|
26
- @index.send("#{key}=", value) if @index.class.settings.include?(key)
26
+ @index.send("#{key}=", value) if @index.class.settings.include?(key)
27
+ @index.options[key] = value if search_option?(key)
27
28
  end
28
29
  end
29
30
 
@@ -4,7 +4,7 @@ class ThinkingSphinx::RealTime::Transcriber
4
4
  end
5
5
 
6
6
  def copy(instance)
7
- return unless copy? instance
7
+ return unless instance.persisted? && copy?(instance)
8
8
 
9
9
  columns, values = ['id'], [index.document_id_for_key(instance.id)]
10
10
  (index.fields + index.attributes).each do |property|
@@ -74,6 +74,10 @@ class ThinkingSphinx::Search < Array
74
74
  @populated = true
75
75
  end
76
76
 
77
+ def populated?
78
+ @populated
79
+ end
80
+
77
81
  def query_time
78
82
  meta['time'].to_f
79
83
  end
@@ -6,6 +6,10 @@ class ThinkingSphinx::Search::Merger
6
6
  end
7
7
 
8
8
  def merge!(query = nil, options = {})
9
+ if search.populated?
10
+ raise ThinkingSphinx::PopulatedResultsError, 'This search request has already been made - you can no longer modify it.'
11
+ end
12
+
9
13
  query, options = nil, query if query.is_a?(Hash)
10
14
  @search.query = query unless query.nil?
11
15
 
@@ -2,15 +2,21 @@ module ThinkingSphinx::SphinxQL
2
2
  mattr_accessor :weight, :group_by, :count
3
3
 
4
4
  def self.functions!
5
- self.weight = 'weight()'
6
- self.group_by = 'groupby()'
7
- self.count = 'count(*)'
5
+ self.weight = {:select => 'weight()', :column => 'weight()'}
6
+ self.group_by = {
7
+ :select => 'groupby() AS sphinx_internal_group',
8
+ :column => 'sphinx_internal_group'
9
+ }
10
+ self.count = {
11
+ :select => 'id AS sphinx_document_id, count(DISTINCT sphinx_document_id) AS sphinx_internal_count',
12
+ :column => 'sphinx_internal_count'
13
+ }
8
14
  end
9
15
 
10
16
  def self.variables!
11
- self.weight = '@weight'
12
- self.group_by = '@groupby'
13
- self.count = '@count'
17
+ self.weight = {:select => '@weight', :column => '@weight'}
18
+ self.group_by = {:select => '@groupby', :column => '@groupby'}
19
+ self.count = {:select => '@count', :column => '@count'}
14
20
  end
15
21
 
16
22
  self.functions!
@@ -14,7 +14,12 @@ namespace :ts do
14
14
 
15
15
  desc 'Clear out Sphinx files'
16
16
  task :clear => :environment do
17
- interface.clear
17
+ interface.clear_all
18
+ end
19
+
20
+ desc 'Clear out real-time index files'
21
+ task :clear_rt => :environment do
22
+ interface.clear_real_time
18
23
  end
19
24
 
20
25
  desc 'Generate fresh index files for real-time indices'
@@ -24,10 +29,10 @@ namespace :ts do
24
29
  end
25
30
 
26
31
  desc 'Stop Sphinx, index and then restart Sphinx'
27
- task :rebuild => [:stop, :index, :start]
32
+ task :rebuild => [:stop, :clear, :index, :start]
28
33
 
29
34
  desc 'Stop Sphinx, clear files, reconfigure, start Sphinx, generate files'
30
- task :regenerate => [:stop, :clear, :configure, :start, :generate]
35
+ task :regenerate => [:stop, :clear_rt, :configure, :start, :generate]
31
36
 
32
37
  desc 'Restart the Sphinx daemon'
33
38
  task :restart => [:stop, :start]
@@ -42,6 +47,11 @@ namespace :ts do
42
47
  interface.stop
43
48
  end
44
49
 
50
+ desc 'Determine whether Sphinx is running'
51
+ task :status => :environment do
52
+ interface.status
53
+ end
54
+
45
55
  def interface
46
56
  @interface ||= ThinkingSphinx::RakeInterface.new
47
57
  end
@@ -16,7 +16,7 @@ describe 'Accessing attributes directly via search results', :live => true do
16
16
  index
17
17
 
18
18
  search = Book.search 'gods',
19
- :select => "*, #{ThinkingSphinx::SphinxQL.weight}"
19
+ :select => "*, #{ThinkingSphinx::SphinxQL.weight[:select]}"
20
20
  search.context[:panes] << ThinkingSphinx::Panes::WeightPane
21
21
 
22
22
  search.first.weight.should == 2500
@@ -27,7 +27,7 @@ describe 'Accessing attributes directly via search results', :live => true do
27
27
  index
28
28
 
29
29
  search = Book.search 'gods',
30
- :select => "*, #{ThinkingSphinx::SphinxQL.weight}"
30
+ :select => "*, #{ThinkingSphinx::SphinxQL.weight[:select]}"
31
31
  search.masks << ThinkingSphinx::Masks::WeightEnumeratorMask
32
32
 
33
33
  expectations = [[gods, 2500]]
@@ -25,3 +25,14 @@ describe '64 bit integer support' do
25
25
  }.type.should == :bigint
26
26
  end
27
27
  end
28
+
29
+ describe '64 bit document ids', :live => true do
30
+ it 'handles large 32 bit integers with an offset multiplier' do
31
+ user = User.create! :name => 'Pat'
32
+ user.update_column :id, 980190962
33
+
34
+ index
35
+
36
+ expect(User.search('pat').to_a).to eq([user])
37
+ end
38
+ end
@@ -26,10 +26,13 @@ describe 'Faceted searching', :live => true do
26
26
  Tee.create!
27
27
  City.create!
28
28
  Product.create!
29
+ Article.create!
29
30
  index
30
31
 
32
+ article_count = ENV['SPHINX_VERSION'].try(:[], /2.0.\d/) ? 2 : 1
33
+
31
34
  ThinkingSphinx.facets.to_hash[:class].should == {
32
- 'Tee' => 2, 'City' => 1, 'Product' => 1
35
+ 'Tee' => 2, 'City' => 1, 'Product' => 1, 'Article' => article_count
33
36
  }
34
37
  end
35
38