thinking-sphinx 3.1.0 → 3.1.1

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/.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