thinking-sphinx 3.1.0 → 3.1.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +4 -7
  4. data/HISTORY +27 -0
  5. data/README.textile +38 -218
  6. data/gemfiles/rails_3_2.gemfile +2 -3
  7. data/gemfiles/rails_4_0.gemfile +2 -3
  8. data/gemfiles/rails_4_1.gemfile +2 -3
  9. data/lib/thinking_sphinx.rb +1 -0
  10. data/lib/thinking_sphinx/active_record.rb +1 -0
  11. data/lib/thinking_sphinx/active_record/association_proxy.rb +1 -0
  12. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +5 -10
  13. data/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb +38 -0
  14. data/lib/thinking_sphinx/active_record/attribute/type.rb +19 -8
  15. data/lib/thinking_sphinx/active_record/base.rb +3 -1
  16. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +1 -1
  17. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +4 -1
  18. data/lib/thinking_sphinx/active_record/index.rb +4 -4
  19. data/lib/thinking_sphinx/active_record/property_query.rb +57 -27
  20. data/lib/thinking_sphinx/active_record/simple_many_query.rb +35 -0
  21. data/lib/thinking_sphinx/capistrano/v3.rb +11 -10
  22. data/lib/thinking_sphinx/configuration.rb +23 -6
  23. data/lib/thinking_sphinx/connection.rb +8 -9
  24. data/lib/thinking_sphinx/errors.rb +7 -2
  25. data/lib/thinking_sphinx/facet.rb +2 -2
  26. data/lib/thinking_sphinx/facet_search.rb +4 -2
  27. data/lib/thinking_sphinx/logger.rb +7 -0
  28. data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -4
  29. data/lib/thinking_sphinx/middlewares/inquirer.rb +2 -2
  30. data/lib/thinking_sphinx/middlewares/sphinxql.rb +6 -2
  31. data/lib/thinking_sphinx/middlewares/stale_id_filter.rb +1 -1
  32. data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +6 -1
  33. data/lib/thinking_sphinx/real_time/property.rb +2 -1
  34. data/lib/thinking_sphinx/real_time/transcriber.rb +7 -3
  35. data/lib/thinking_sphinx/search.rb +14 -4
  36. data/lib/thinking_sphinx/search/context.rb +0 -6
  37. data/lib/thinking_sphinx/test.rb +11 -2
  38. data/lib/thinking_sphinx/wildcard.rb +7 -1
  39. data/spec/acceptance/association_scoping_spec.rb +55 -15
  40. data/spec/acceptance/geosearching_spec.rb +8 -2
  41. data/spec/acceptance/real_time_updates_spec.rb +9 -0
  42. data/spec/acceptance/specifying_sql_spec.rb +31 -17
  43. data/spec/internal/app/indices/car_index.rb +5 -0
  44. data/spec/internal/app/models/car.rb +5 -0
  45. data/spec/internal/app/models/category.rb +2 -1
  46. data/spec/internal/app/models/manufacturer.rb +3 -0
  47. data/spec/internal/db/schema.rb +9 -0
  48. data/spec/thinking_sphinx/active_record/attribute/type_spec.rb +7 -0
  49. data/spec/thinking_sphinx/active_record/base_spec.rb +17 -0
  50. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +2 -1
  51. data/spec/thinking_sphinx/configuration_spec.rb +40 -2
  52. data/spec/thinking_sphinx/errors_spec.rb +21 -0
  53. data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
  54. data/spec/thinking_sphinx/middlewares/inquirer_spec.rb +0 -4
  55. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +25 -0
  56. data/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb +2 -2
  57. data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +2 -1
  58. data/spec/thinking_sphinx/search_spec.rb +56 -0
  59. data/spec/thinking_sphinx/wildcard_spec.rb +5 -0
  60. data/thinking-sphinx.gemspec +2 -2
  61. metadata +40 -32
  62. data/sketchpad.rb +0 -58
  63. data/spec/internal/log/.gitignore +0 -1
@@ -12,7 +12,7 @@ module ThinkingSphinx::Connection
12
12
  :reconnect => true
13
13
  }.merge(configuration.settings['connection_options'] || {})
14
14
 
15
- connection_class.new address, options[:port], options
15
+ connection_class.new options
16
16
  end
17
17
 
18
18
  def self.connection_class
@@ -85,7 +85,8 @@ module ThinkingSphinx::Connection
85
85
  def query(*statements)
86
86
  results_for *statements
87
87
  rescue => error
88
- wrapper = ThinkingSphinx::QueryExecutionError.new error.message
88
+ message = "#{error.message} - #{statements.join('; ')}"
89
+ wrapper = ThinkingSphinx::QueryExecutionError.new message
89
90
  wrapper.statement = statements.join('; ')
90
91
  raise wrapper
91
92
  ensure
@@ -94,8 +95,8 @@ module ThinkingSphinx::Connection
94
95
  end
95
96
 
96
97
  class MRI < Client
97
- def initialize(address, port, options)
98
- @address, @port, @options = address, port, options
98
+ def initialize(options)
99
+ @options = options
99
100
  end
100
101
 
101
102
  def base_error
@@ -104,12 +105,10 @@ module ThinkingSphinx::Connection
104
105
 
105
106
  private
106
107
 
107
- attr_reader :address, :port, :options
108
+ attr_reader :options
108
109
 
109
110
  def client
110
111
  @client ||= Mysql2::Client.new({
111
- :host => address,
112
- :port => port,
113
112
  :flags => Mysql2::Client::MULTI_STATEMENTS
114
113
  }.merge(options))
115
114
  rescue base_error => error
@@ -126,8 +125,8 @@ module ThinkingSphinx::Connection
126
125
  class JRuby < Client
127
126
  attr_reader :address, :options
128
127
 
129
- def initialize(address, port, options)
130
- @address = "jdbc:mysql://#{address}:#{port}?allowMultiQueries=true"
128
+ def initialize(options)
129
+ @address = "jdbc:mysql://#{options[:host]}:#{options[:port]}/?allowMultiQueries=true"
131
130
  @options = options
132
131
  end
133
132
 
@@ -9,8 +9,10 @@ class ThinkingSphinx::SphinxError < StandardError
9
9
  replacement = ThinkingSphinx::SyntaxError.new(error.message)
10
10
  when /query error/
11
11
  replacement = ThinkingSphinx::QueryError.new(error.message)
12
- when /Can't connect to MySQL server/
13
- replacement = ThinkingSphinx::ConnectionError.new(error.message)
12
+ when /Can't connect to MySQL server/, /Communications link failure/
13
+ replacement = ThinkingSphinx::ConnectionError.new(
14
+ "Error connecting to Sphinx via the MySQL protocol. #{error.message}"
15
+ )
14
16
  else
15
17
  replacement = new(error.message)
16
18
  end
@@ -42,3 +44,6 @@ end
42
44
 
43
45
  class ThinkingSphinx::NoIndicesError < StandardError
44
46
  end
47
+
48
+ class ThinkingSphinx::MissingColumnError < StandardError
49
+ 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[ThinkingSphinx::SphinxQL.count]
14
+ hash[row[group_column]] = row['sphinx_internal_count']
15
15
  hash
16
16
  }
17
17
  end
@@ -19,7 +19,7 @@ class ThinkingSphinx::Facet
19
19
  private
20
20
 
21
21
  def group_column
22
- @properties.any?(&:multi?) ? ThinkingSphinx::SphinxQL.group_by : name
22
+ @properties.any?(&:multi?) ? 'sphinx_internal_group' : name
23
23
  end
24
24
 
25
25
  def use_field?
@@ -101,8 +101,10 @@ class ThinkingSphinx::FacetSearch
101
101
 
102
102
  def options_for(facet)
103
103
  options.merge(
104
- :select => (options[:select] || '*') +
105
- ", #{ThinkingSphinx::SphinxQL.group_by}, #{ThinkingSphinx::SphinxQL.count}",
104
+ :select => [(options[:select] || '*'),
105
+ "#{ThinkingSphinx::SphinxQL.group_by} as sphinx_internal_group",
106
+ "#{ThinkingSphinx::SphinxQL.count} as sphinx_internal_count"
107
+ ].join(', '),
106
108
  :group_by => facet.name,
107
109
  :indices => index_names_for(facet),
108
110
  :max_matches => max_matches,
@@ -0,0 +1,7 @@
1
+ class ThinkingSphinx::Logger
2
+ def self.log(notification, message, &block)
3
+ ActiveSupport::Notifications.instrument(
4
+ "#{notification}.thinking_sphinx", notification => message, &block
5
+ )
6
+ end
7
+ 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[ThinkingSphinx::SphinxQL.count]
12
+ yield @search[index], row['sphinx_internal_count']
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[ThinkingSphinx::SphinxQL.group_by]
18
+ yield @search[index], row['sphinx_internal_group']
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[ThinkingSphinx::SphinxQL.group_by],
25
- row[ThinkingSphinx::SphinxQL.count]
24
+ yield @search[index], row['sphinx_internal_group'],
25
+ row['sphinx_internal_count']
26
26
  end
27
27
  end
28
28
  end
@@ -5,7 +5,7 @@ class ThinkingSphinx::Middlewares::Inquirer <
5
5
  @contexts = contexts
6
6
  @batch = nil
7
7
 
8
- contexts.first.log :query, combined_queries do
8
+ ThinkingSphinx::Logger.log :query, combined_queries do
9
9
  batch.results
10
10
  end
11
11
 
@@ -52,7 +52,7 @@ class ThinkingSphinx::Middlewares::Inquirer <
52
52
  }
53
53
 
54
54
  total = context[:meta]['total_found']
55
- context.log :message, "Found #{total} result#{'s' unless total == 1}"
55
+ ThinkingSphinx::Logger.log :message, "Found #{total} result#{'s' unless total == 1}"
56
56
  end
57
57
 
58
58
  private
@@ -65,7 +65,8 @@ class ThinkingSphinx::Middlewares::SphinxQL <
65
65
  end
66
66
 
67
67
  def constantize_inheritance_column(klass)
68
- klass.connection.select_values(inheritance_column_select(klass)).compact.each(&:constantize)
68
+ values = klass.connection.select_values inheritance_column_select(klass)
69
+ values.reject(&:blank?).each(&:constantize)
69
70
  end
70
71
 
71
72
  def descendants
@@ -155,7 +156,10 @@ SQL
155
156
  end
156
157
 
157
158
  def values
158
- options[:select] ||= "*, #{ThinkingSphinx::SphinxQL.group_by}, #{ThinkingSphinx::SphinxQL.count}" if group_attribute.present?
159
+ options[:select] ||= ['*',
160
+ "#{ThinkingSphinx::SphinxQL.group_by} as sphinx_internal_group",
161
+ "#{ThinkingSphinx::SphinxQL.count} as sphinx_internal_count"
162
+ ].join(', ') if group_attribute.present?
159
163
  options[:select]
160
164
  end
161
165
 
@@ -12,7 +12,7 @@ class ThinkingSphinx::Middlewares::StaleIdFilter <
12
12
  raise error if @retries <= 0
13
13
 
14
14
  append_stale_ids error.ids
15
- @context.log :message, log_message
15
+ ThinkingSphinx::Logger.log :message, log_message
16
16
 
17
17
  @retries -= 1 and retry
18
18
  end
@@ -4,7 +4,7 @@ class ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks
4
4
  end
5
5
 
6
6
  def after_save(instance)
7
- return unless real_time_indices?
7
+ return unless real_time_indices? && callbacks_enabled?
8
8
 
9
9
  real_time_indices.each do |index|
10
10
  objects_for(instance).each do |object|
@@ -17,6 +17,11 @@ class ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks
17
17
 
18
18
  attr_reader :reference, :path
19
19
 
20
+ def callbacks_enabled?
21
+ setting = configuration.settings['real_time_callbacks']
22
+ setting.nil? || setting
23
+ end
24
+
20
25
  def configuration
21
26
  ThinkingSphinx::Configuration.instance
22
27
  end
@@ -17,6 +17,7 @@ class ThinkingSphinx::RealTime::Property
17
17
  return @column.__name unless @column.__name.is_a?(Symbol)
18
18
 
19
19
  base = @column.__stack.inject(object) { |base, node| base.try(node) }
20
- base.try(@column.__name)
20
+ base = base.try(@column.__name)
21
+ base.is_a?(String) ? base.gsub("\u0000", '') : base
21
22
  end
22
23
  end
@@ -12,9 +12,13 @@ class ThinkingSphinx::RealTime::Transcriber
12
12
  values << property.translate(instance)
13
13
  end
14
14
 
15
- sphinxql = Riddle::Query::Insert.new index.name, columns, values
16
- ThinkingSphinx::Connection.take do |connection|
17
- connection.execute sphinxql.replace!.to_sql
15
+ insert = Riddle::Query::Insert.new index.name, columns, values
16
+ sphinxql = insert.replace!.to_sql
17
+
18
+ ThinkingSphinx::Logger.log :query, sphinxql do
19
+ ThinkingSphinx::Connection.take do |connection|
20
+ connection.execute sphinxql
21
+ end
18
22
  end
19
23
  end
20
24
 
@@ -83,10 +83,6 @@ class ThinkingSphinx::Search < Array
83
83
  context[:raw]
84
84
  end
85
85
 
86
- def respond_to?(method, include_private = false)
87
- super || context[:results].respond_to?(method, include_private)
88
- end
89
-
90
86
  def to_a
91
87
  populate
92
88
  context[:results].collect { |result|
@@ -105,6 +101,10 @@ class ThinkingSphinx::Search < Array
105
101
  @mask_stack ||= masks.collect { |klass| klass.new self }
106
102
  end
107
103
 
104
+ def masks_respond_to?(method)
105
+ mask_stack.any? { |mask| mask.can_handle? method }
106
+ end
107
+
108
108
  def method_missing(method, *args, &block)
109
109
  mask_stack.each do |mask|
110
110
  return mask.send(method, *args, &block) if mask.can_handle?(method)
@@ -115,9 +115,19 @@ class ThinkingSphinx::Search < Array
115
115
  context[:results].send(method, *args, &block)
116
116
  end
117
117
 
118
+ def respond_to_missing?(method, include_private = false)
119
+ super ||
120
+ masks_respond_to?(method) ||
121
+ results_respond_to?(method, include_private)
122
+ end
123
+
118
124
  def middleware
119
125
  @options[:middleware] || default_middleware
120
126
  end
127
+
128
+ def results_respond_to?(method, include_private = true)
129
+ context[:results].respond_to?(method, include_private)
130
+ end
121
131
  end
122
132
 
123
133
  require 'thinking_sphinx/search/batch_inquirer'
@@ -17,10 +17,4 @@ class ThinkingSphinx::Search::Context
17
17
  def []=(key, value)
18
18
  @memory[key] = value
19
19
  end
20
-
21
- def log(notification, message, &block)
22
- ActiveSupport::Notifications.instrument(
23
- "#{notification}.thinking_sphinx", notification => message, &block
24
- )
25
- end
26
20
  end
@@ -4,9 +4,9 @@ class ThinkingSphinx::Test
4
4
  config.settings['quiet_deltas'] = suppress_delta_output
5
5
  end
6
6
 
7
- def self.start
7
+ def self.start(options = {})
8
8
  config.render_to_file
9
- config.controller.index
9
+ config.controller.index if options[:index].nil? || options[:index]
10
10
  config.controller.start
11
11
  end
12
12
 
@@ -35,6 +35,15 @@ class ThinkingSphinx::Test
35
35
  end
36
36
  end
37
37
 
38
+ def self.clear
39
+ [
40
+ config.indices_location,
41
+ config.searchd.binlog_path
42
+ ].each do |path|
43
+ FileUtils.rm_r(path) if File.exists?(path)
44
+ end
45
+ end
46
+
38
47
  def self.config
39
48
  @config ||= ::ThinkingSphinx::Configuration.instance
40
49
  end
@@ -11,7 +11,7 @@ class ThinkingSphinx::Wildcard
11
11
  end
12
12
 
13
13
  def call
14
- query.gsub(/("#{pattern}(.*?#{pattern})?"|(?![!-])#{pattern})/u) do
14
+ query.gsub(extended_pattern) do
15
15
  pre, proper, post = $`, $&, $'
16
16
  # E.g. "@foo", "/2", "~3", but not as part of a token pattern
17
17
  is_operator = pre == '@' ||
@@ -31,4 +31,10 @@ class ThinkingSphinx::Wildcard
31
31
  private
32
32
 
33
33
  attr_reader :query, :pattern
34
+
35
+ def extended_pattern
36
+ Regexp.new(
37
+ "(\"#{pattern}(.*?#{pattern})?\"|(?![!-])#{pattern})".encode('UTF-8')
38
+ )
39
+ end
34
40
  end
@@ -1,23 +1,63 @@
1
1
  require 'acceptance/spec_helper'
2
2
 
3
3
  describe 'Scoping association search calls by foreign keys', :live => true do
4
- it "limits results to those matching the foreign key" do
5
- pat = User.create :name => 'Pat'
6
- melbourne = Article.create :title => 'Guide to Melbourne', :user => pat
7
- paul = User.create :name => 'Paul'
8
- dublin = Article.create :title => 'Guide to Dublin', :user => paul
9
- index
10
-
11
- pat.articles.search('Guide').to_a.should == [melbourne]
4
+ describe 'for ActiveRecord indices' do
5
+ it "limits results to those matching the foreign key" do
6
+ pat = User.create :name => 'Pat'
7
+ melbourne = Article.create :title => 'Guide to Melbourne', :user => pat
8
+ paul = User.create :name => 'Paul'
9
+ dublin = Article.create :title => 'Guide to Dublin', :user => paul
10
+ index
11
+
12
+ pat.articles.search('Guide').to_a.should == [melbourne]
13
+ end
14
+
15
+ it "limits id-only results to those matching the foreign key" do
16
+ pat = User.create :name => 'Pat'
17
+ melbourne = Article.create :title => 'Guide to Melbourne', :user => pat
18
+ paul = User.create :name => 'Paul'
19
+ dublin = Article.create :title => 'Guide to Dublin', :user => paul
20
+ index
21
+
22
+ pat.articles.search_for_ids('Guide').to_a.should == [melbourne.id]
23
+ end
24
+ end
25
+
26
+ describe 'for real-time indices' do
27
+ it "limits results to those matching the foreign key" do
28
+ porsche = Manufacturer.create :name => 'Porsche'
29
+ spyder = Car.create :name => '918 Spyder', :manufacturer => porsche
30
+
31
+ audi = Manufacturer.create :name => 'Audi'
32
+ r_eight = Car.create :name => 'R8 Spyder', :manufacturer => audi
33
+
34
+ porsche.cars.search('Spyder').to_a.should == [spyder]
35
+ end
36
+
37
+ it "limits id-only results to those matching the foreign key" do
38
+ porsche = Manufacturer.create :name => 'Porsche'
39
+ spyder = Car.create :name => '918 Spyder', :manufacturer => porsche
40
+
41
+ audi = Manufacturer.create :name => 'Audi'
42
+ r_eight = Car.create :name => 'R8 Spyder', :manufacturer => audi
43
+
44
+ porsche.cars.search_for_ids('Spyder').to_a.should == [spyder.id]
45
+ end
12
46
  end
13
47
 
14
- it "limits id-only results to those matching the foreign key" do
15
- pat = User.create :name => 'Pat'
16
- melbourne = Article.create :title => 'Guide to Melbourne', :user => pat
17
- paul = User.create :name => 'Paul'
18
- dublin = Article.create :title => 'Guide to Dublin', :user => paul
19
- index
48
+ describe 'with has_many :through associations' do
49
+ it 'limits results to those matching the foreign key' do
50
+ pancakes = Product.create :name => 'Low fat Pancakes'
51
+ waffles = Product.create :name => 'Low fat Waffles'
52
+
53
+ food = Category.create :name => 'food'
54
+ flat = Category.create :name => 'flat'
55
+
56
+ pancakes.categories << food
57
+ pancakes.categories << flat
58
+ waffles.categories << food
20
59
 
21
- pat.articles.search_for_ids('Guide').to_a.should == [melbourne.id]
60
+ flat.products.search('Low').to_a.should == [pancakes]
61
+ end
22
62
  end
23
63
  end
@@ -30,10 +30,16 @@ describe 'Searching by latitude and longitude', :live => true do
30
30
  index
31
31
 
32
32
  cities = City.search(:geo => [-0.616241, 2.602712], :order => 'geodist ASC')
33
+ if ENV['SPHINX_VERSION'].try :[], /2.2.\d/
34
+ expected = {:mysql => 249907.171875, :postgresql => 249912.03125}
35
+ else
36
+ expected = {:mysql => 250326.906250, :postgresql => 250331.234375}
37
+ end
38
+
33
39
  if ActiveRecord::Base.configurations['test']['adapter'][/postgres/]
34
- cities.first.geodist.should == 250331.234375
40
+ cities.first.geodist.should == expected[:postgresql]
35
41
  else # mysql
36
- cities.first.geodist.should == 250326.906250
42
+ cities.first.geodist.should == expected[:mysql]
37
43
  end
38
44
  end
39
45
 
@@ -0,0 +1,9 @@
1
+ require 'acceptance/spec_helper'
2
+
3
+ describe 'Updates to records in real-time indices', :live => true do
4
+ it "handles fields with unicode nulls" do
5
+ product = Product.create! :name => "Widget \u0000"
6
+
7
+ Product.search.first.should == product
8
+ end
9
+ end