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.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.travis.yml +4 -7
- data/HISTORY +27 -0
- data/README.textile +38 -218
- data/gemfiles/rails_3_2.gemfile +2 -3
- data/gemfiles/rails_4_0.gemfile +2 -3
- data/gemfiles/rails_4_1.gemfile +2 -3
- data/lib/thinking_sphinx.rb +1 -0
- data/lib/thinking_sphinx/active_record.rb +1 -0
- data/lib/thinking_sphinx/active_record/association_proxy.rb +1 -0
- data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +5 -10
- data/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb +38 -0
- data/lib/thinking_sphinx/active_record/attribute/type.rb +19 -8
- data/lib/thinking_sphinx/active_record/base.rb +3 -1
- data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +1 -1
- data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +4 -1
- data/lib/thinking_sphinx/active_record/index.rb +4 -4
- data/lib/thinking_sphinx/active_record/property_query.rb +57 -27
- data/lib/thinking_sphinx/active_record/simple_many_query.rb +35 -0
- data/lib/thinking_sphinx/capistrano/v3.rb +11 -10
- data/lib/thinking_sphinx/configuration.rb +23 -6
- data/lib/thinking_sphinx/connection.rb +8 -9
- data/lib/thinking_sphinx/errors.rb +7 -2
- data/lib/thinking_sphinx/facet.rb +2 -2
- data/lib/thinking_sphinx/facet_search.rb +4 -2
- data/lib/thinking_sphinx/logger.rb +7 -0
- data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -4
- data/lib/thinking_sphinx/middlewares/inquirer.rb +2 -2
- data/lib/thinking_sphinx/middlewares/sphinxql.rb +6 -2
- data/lib/thinking_sphinx/middlewares/stale_id_filter.rb +1 -1
- data/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +6 -1
- data/lib/thinking_sphinx/real_time/property.rb +2 -1
- data/lib/thinking_sphinx/real_time/transcriber.rb +7 -3
- data/lib/thinking_sphinx/search.rb +14 -4
- data/lib/thinking_sphinx/search/context.rb +0 -6
- data/lib/thinking_sphinx/test.rb +11 -2
- data/lib/thinking_sphinx/wildcard.rb +7 -1
- data/spec/acceptance/association_scoping_spec.rb +55 -15
- data/spec/acceptance/geosearching_spec.rb +8 -2
- data/spec/acceptance/real_time_updates_spec.rb +9 -0
- data/spec/acceptance/specifying_sql_spec.rb +31 -17
- data/spec/internal/app/indices/car_index.rb +5 -0
- data/spec/internal/app/models/car.rb +5 -0
- data/spec/internal/app/models/category.rb +2 -1
- data/spec/internal/app/models/manufacturer.rb +3 -0
- data/spec/internal/db/schema.rb +9 -0
- data/spec/thinking_sphinx/active_record/attribute/type_spec.rb +7 -0
- data/spec/thinking_sphinx/active_record/base_spec.rb +17 -0
- data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +2 -1
- data/spec/thinking_sphinx/configuration_spec.rb +40 -2
- data/spec/thinking_sphinx/errors_spec.rb +21 -0
- data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
- data/spec/thinking_sphinx/middlewares/inquirer_spec.rb +0 -4
- data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +25 -0
- data/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb +2 -2
- data/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +2 -1
- data/spec/thinking_sphinx/search_spec.rb +56 -0
- data/spec/thinking_sphinx/wildcard_spec.rb +5 -0
- data/thinking-sphinx.gemspec +2 -2
- metadata +40 -32
- data/sketchpad.rb +0 -58
- 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
|
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
|
-
|
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(
|
98
|
-
@
|
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 :
|
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(
|
130
|
-
@address = "jdbc:mysql://#{
|
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(
|
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[
|
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?) ?
|
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
|
-
"
|
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,
|
@@ -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[
|
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[
|
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[
|
25
|
-
row[
|
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
|
-
|
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
|
-
|
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
|
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] ||=
|
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
|
|
@@ -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
|
-
|
16
|
-
|
17
|
-
|
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
|
data/lib/thinking_sphinx/test.rb
CHANGED
@@ -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(
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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 ==
|
40
|
+
cities.first.geodist.should == expected[:postgresql]
|
35
41
|
else # mysql
|
36
|
-
cities.first.geodist.should ==
|
42
|
+
cities.first.geodist.should == expected[:mysql]
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|