thinking-sphinx 3.1.1 → 3.1.2

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