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