thinking-sphinx 2.0.0.rc2 → 2.0.0
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.
- data/README.textile +6 -0
- data/VERSION +1 -1
- data/features/excerpts.feature +8 -0
- data/features/field_sorting.feature +18 -0
- data/features/searching_across_models.feature +1 -1
- data/features/searching_by_model.feature +0 -7
- data/features/sphinx_scopes.feature +18 -0
- data/features/step_definitions/common_steps.rb +4 -0
- data/features/step_definitions/search_steps.rb +5 -0
- data/features/support/env.rb +4 -5
- data/features/thinking_sphinx/db/fixtures/people.rb +1 -1
- data/features/thinking_sphinx/models/alpha.rb +1 -0
- data/features/thinking_sphinx/models/andrew.rb +17 -0
- data/features/thinking_sphinx/models/person.rb +2 -1
- data/lib/thinking_sphinx.rb +3 -0
- data/lib/thinking_sphinx/active_record.rb +1 -1
- data/lib/thinking_sphinx/active_record/scopes.rb +7 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +38 -8
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +6 -2
- data/lib/thinking_sphinx/association.rb +19 -14
- data/lib/thinking_sphinx/attribute.rb +5 -0
- data/lib/thinking_sphinx/auto_version.rb +2 -0
- data/lib/thinking_sphinx/bundled_search.rb +44 -0
- data/lib/thinking_sphinx/configuration.rb +14 -10
- data/lib/thinking_sphinx/context.rb +4 -2
- data/lib/thinking_sphinx/property.rb +1 -0
- data/lib/thinking_sphinx/railtie.rb +2 -2
- data/lib/thinking_sphinx/search.rb +74 -48
- data/lib/thinking_sphinx/source/sql.rb +1 -1
- data/lib/thinking_sphinx/tasks.rb +7 -0
- data/spec/thinking_sphinx/active_record/scopes_spec.rb +2 -3
- data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +134 -0
- data/spec/thinking_sphinx/association_spec.rb +1 -24
- data/spec/thinking_sphinx/auto_version_spec.rb +8 -0
- data/spec/thinking_sphinx/configuration_spec.rb +11 -4
- data/spec/thinking_sphinx/context_spec.rb +3 -2
- data/spec/thinking_sphinx/search_spec.rb +67 -25
- data/tasks/distribution.rb +0 -6
- data/tasks/testing.rb +25 -15
- metadata +279 -67
@@ -53,15 +53,16 @@ module ThinkingSphinx
|
|
53
53
|
mysql_ssl_ca sql_range_step sql_query_pre sql_query_post
|
54
54
|
sql_query_killlist sql_ranged_throttle sql_query_post_index unpack_zlib
|
55
55
|
unpack_mysqlcompress unpack_mysqlcompress_maxsize )
|
56
|
-
|
57
|
-
IndexOptions = %w( charset_table charset_type charset_dictpath
|
58
|
-
enable_star exceptions
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
56
|
+
|
57
|
+
IndexOptions = %w( blend_chars charset_table charset_type charset_dictpath
|
58
|
+
docinfo enable_star exceptions expand_keywords hitless_words
|
59
|
+
html_index_attrs html_remove_elements html_strip index_exact_words
|
60
|
+
ignore_chars inplace_docinfo_gap inplace_enable inplace_hit_gap
|
61
|
+
inplace_reloc_factor inplace_write_factor min_infix_len min_prefix_len
|
62
|
+
min_stemming_len min_word_len mlock morphology ngram_chars ngram_len
|
63
|
+
ondisk_dict overshort_step phrase_boundary phrase_boundary_step preopen
|
64
|
+
stopwords stopwords_step wordforms )
|
65
|
+
|
65
66
|
CustomOptions = %w( disable_range )
|
66
67
|
|
67
68
|
attr_accessor :searchd_file_path, :allow_star, :database_yml_file,
|
@@ -232,10 +233,13 @@ module ThinkingSphinx
|
|
232
233
|
def indexer_binary_name=(name)
|
233
234
|
@controller.indexer_binary_name = name
|
234
235
|
end
|
235
|
-
|
236
|
+
|
237
|
+
attr_accessor :timeout
|
238
|
+
|
236
239
|
def client
|
237
240
|
client = Riddle::Client.new address, port
|
238
241
|
client.max_matches = configuration.searchd.max_matches || 1000
|
242
|
+
client.timeout = timeout || 0
|
239
243
|
client
|
240
244
|
end
|
241
245
|
|
@@ -65,8 +65,10 @@ class ThinkingSphinx::Context
|
|
65
65
|
model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
|
66
66
|
rescue NameError
|
67
67
|
next
|
68
|
-
rescue StandardError
|
69
|
-
STDERR.puts "Warning: Error loading #{file}"
|
68
|
+
rescue StandardError => err
|
69
|
+
STDERR.puts "Warning: Error loading #{file}:"
|
70
|
+
STDERR.puts err.message
|
71
|
+
STDERR.puts err.backtrace.join("\n"), ''
|
70
72
|
end
|
71
73
|
end
|
72
74
|
end
|
@@ -5,8 +5,8 @@ module ThinkingSphinx
|
|
5
5
|
class Railtie < Rails::Railtie
|
6
6
|
|
7
7
|
initializer "thinking_sphinx.active_record" do
|
8
|
-
|
9
|
-
|
8
|
+
ActiveSupport.on_load :active_record do
|
9
|
+
include ThinkingSphinx::ActiveRecord
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
@@ -57,20 +57,16 @@ module ThinkingSphinx
|
|
57
57
|
ThinkingSphinx.facets *args
|
58
58
|
end
|
59
59
|
|
60
|
-
def self.bundle_searches(enum)
|
61
|
-
|
60
|
+
def self.bundle_searches(enum = nil)
|
61
|
+
bundle = ThinkingSphinx::BundledSearch.new
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
client.run.each_with_index.collect { |results, index|
|
70
|
-
searches[index].populate_from_queue results
|
71
|
-
}
|
63
|
+
if enum.nil?
|
64
|
+
yield bundle
|
65
|
+
else
|
66
|
+
enum.each { |item| yield bundle, item }
|
67
|
+
end
|
72
68
|
|
73
|
-
searches
|
69
|
+
bundle.searches
|
74
70
|
end
|
75
71
|
|
76
72
|
def self.matching_fields(fields, bitmask)
|
@@ -90,6 +86,8 @@ module ThinkingSphinx
|
|
90
86
|
@options = args.extract_options!
|
91
87
|
@args = args
|
92
88
|
|
89
|
+
add_default_scope unless options[:ignore_default]
|
90
|
+
|
93
91
|
populate if @options[:populate]
|
94
92
|
end
|
95
93
|
|
@@ -127,7 +125,7 @@ module ThinkingSphinx
|
|
127
125
|
add_scope(method, *args, &block)
|
128
126
|
return self
|
129
127
|
elsif method == :search_count
|
130
|
-
merge_search one_class.search(*args)
|
128
|
+
merge_search one_class.search(*args), self.args, options
|
131
129
|
return scoped_count
|
132
130
|
elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
|
133
131
|
super
|
@@ -274,18 +272,43 @@ module ThinkingSphinx
|
|
274
272
|
|
275
273
|
populate
|
276
274
|
client.excerpts(
|
277
|
-
|
278
|
-
|
279
|
-
|
275
|
+
{
|
276
|
+
:docs => [string.to_s],
|
277
|
+
:words => results[:words].keys.join(' '),
|
278
|
+
:index => options[:index] || "#{model.source_of_sphinx_index.sphinx_name}_core"
|
279
|
+
}.merge(options[:excerpt_options] || {})
|
280
280
|
).first
|
281
281
|
end
|
282
282
|
|
283
283
|
def search(*args)
|
284
|
-
|
285
|
-
merge_search ThinkingSphinx::Search.new(*args)
|
284
|
+
args << args.extract_options!.merge(:ignore_default => true)
|
285
|
+
merge_search ThinkingSphinx::Search.new(*args), self.args, options
|
286
286
|
self
|
287
287
|
end
|
288
288
|
|
289
|
+
def search_for_ids(*args)
|
290
|
+
args << args.extract_options!.merge(
|
291
|
+
:ignore_default => true,
|
292
|
+
:ids_only => true
|
293
|
+
)
|
294
|
+
merge_search ThinkingSphinx::Search.new(*args), self.args, options
|
295
|
+
self
|
296
|
+
end
|
297
|
+
|
298
|
+
def facets(*args)
|
299
|
+
options = args.extract_options!
|
300
|
+
merge_search self, args, options
|
301
|
+
args << options
|
302
|
+
|
303
|
+
ThinkingSphinx::FacetSearch.new *args
|
304
|
+
end
|
305
|
+
|
306
|
+
def client
|
307
|
+
client = options[:client] || config.client
|
308
|
+
|
309
|
+
prepare client
|
310
|
+
end
|
311
|
+
|
289
312
|
def append_to(client)
|
290
313
|
prepare client
|
291
314
|
client.append_query query, indexes, comment
|
@@ -386,9 +409,16 @@ module ThinkingSphinx
|
|
386
409
|
|
387
410
|
def self.log(message, method = :debug, identifier = 'Sphinx')
|
388
411
|
return if ::ActiveRecord::Base.logger.nil?
|
389
|
-
|
390
|
-
info =
|
391
|
-
|
412
|
+
|
413
|
+
info = ''
|
414
|
+
if ::ActiveRecord::LogSubscriber.colorize_logging
|
415
|
+
identifier_color, message_color = "4;32;1", "0" # 0;1 = Bold
|
416
|
+
info << " \e[#{identifier_color}m#{identifier}\e[0m "
|
417
|
+
info << "\e[#{message_color}m#{message}\e[0m"
|
418
|
+
else
|
419
|
+
info = "#{identifier} #{message}"
|
420
|
+
end
|
421
|
+
|
392
422
|
::ActiveRecord::Base.logger.send method, info
|
393
423
|
end
|
394
424
|
|
@@ -396,12 +426,6 @@ module ThinkingSphinx
|
|
396
426
|
self.class.log(*args)
|
397
427
|
end
|
398
428
|
|
399
|
-
def client
|
400
|
-
client = config.client
|
401
|
-
|
402
|
-
prepare client
|
403
|
-
end
|
404
|
-
|
405
429
|
def prepare(client)
|
406
430
|
index_options = one_class ?
|
407
431
|
one_class.sphinx_indexes.first.local_options : {}
|
@@ -493,7 +517,8 @@ module ThinkingSphinx
|
|
493
517
|
query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
|
494
518
|
pre, proper, post = $`, $&, $'
|
495
519
|
# E.g. "@foo", "/2", "~3", but not as part of a token
|
496
|
-
is_operator = pre.match(%r{(\W|^)[@~/]\Z})
|
520
|
+
is_operator = pre.match(%r{(\W|^)[@~/]\Z}) ||
|
521
|
+
pre.match(%r{(\W|^)@\([^\)]*$})
|
497
522
|
# E.g. "foo bar", with quotes
|
498
523
|
is_quote = proper.starts_with?('"') && proper.ends_with?('"')
|
499
524
|
has_star = pre.ends_with?("*") || post.starts_with?("*")
|
@@ -607,24 +632,8 @@ module ThinkingSphinx
|
|
607
632
|
filters
|
608
633
|
end
|
609
634
|
|
610
|
-
def condition_filters
|
611
|
-
(options[:conditions] || {}).collect { |attrib, value|
|
612
|
-
if attributes.include?(attrib.to_sym)
|
613
|
-
puts <<-MSG
|
614
|
-
Deprecation Warning: filters on attributes should be done using the :with
|
615
|
-
option, not :conditions. For example:
|
616
|
-
:with => {:#{attrib} => #{value.inspect}}
|
617
|
-
MSG
|
618
|
-
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
619
|
-
else
|
620
|
-
nil
|
621
|
-
end
|
622
|
-
}.compact
|
623
|
-
end
|
624
|
-
|
625
635
|
def filters
|
626
636
|
internal_filters +
|
627
|
-
condition_filters +
|
628
637
|
(options[:with] || {}).collect { |attrib, value|
|
629
638
|
Riddle::Client::Filter.new attrib.to_s, filter_value(value)
|
630
639
|
} +
|
@@ -712,6 +721,21 @@ MSG
|
|
712
721
|
end
|
713
722
|
end
|
714
723
|
|
724
|
+
def include_for_class(klass)
|
725
|
+
includes = options[:include] || klass.sphinx_index_options[:include]
|
726
|
+
|
727
|
+
case includes
|
728
|
+
when NilClass
|
729
|
+
nil
|
730
|
+
when Array
|
731
|
+
includes.select { |inc| klass.reflections[inc] }
|
732
|
+
when Symbol
|
733
|
+
klass.reflections[includes].nil? ? nil : includes
|
734
|
+
else
|
735
|
+
includes
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
715
739
|
def instances_from_class(klass, matches)
|
716
740
|
index_options = klass.sphinx_index_options
|
717
741
|
|
@@ -720,7 +744,7 @@ MSG
|
|
720
744
|
:all,
|
721
745
|
:joins => options[:joins],
|
722
746
|
:conditions => {klass.primary_key_for_sphinx.to_sym => ids},
|
723
|
-
:include => (
|
747
|
+
:include => include_for_class(klass),
|
724
748
|
:select => (options[:select] || index_options[:select]),
|
725
749
|
:order => (options[:sql_order] || index_options[:sql_order])
|
726
750
|
) : []
|
@@ -790,14 +814,16 @@ MSG
|
|
790
814
|
|
791
815
|
# Adds the default_sphinx_scope if set.
|
792
816
|
def add_default_scope
|
793
|
-
|
817
|
+
return unless one_class && one_class.has_default_sphinx_scope?
|
818
|
+
add_scope(one_class.get_default_sphinx_scope.to_sym)
|
794
819
|
end
|
795
820
|
|
796
821
|
def add_scope(method, *args, &block)
|
797
|
-
|
822
|
+
method = "#{method}_without_default".to_sym
|
823
|
+
merge_search one_class.send(method, *args, &block), self.args, options
|
798
824
|
end
|
799
825
|
|
800
|
-
def merge_search(search)
|
826
|
+
def merge_search(search, args, options)
|
801
827
|
search.args.each { |arg| args << arg }
|
802
828
|
|
803
829
|
search.options.keys.each do |key|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'fileutils'
|
2
|
+
require 'timeout'
|
2
3
|
|
3
4
|
namespace :thinking_sphinx do
|
4
5
|
task :app_env do
|
@@ -47,6 +48,12 @@ namespace :thinking_sphinx do
|
|
47
48
|
config = ThinkingSphinx::Configuration.instance
|
48
49
|
pid = sphinx_pid
|
49
50
|
config.controller.stop
|
51
|
+
|
52
|
+
# Ensure searchd is stopped, but don't try too hard
|
53
|
+
Timeout.timeout(5) do
|
54
|
+
sleep(1) until config.controller.stop
|
55
|
+
end
|
56
|
+
|
50
57
|
puts "Stopped search daemon (pid #{pid})."
|
51
58
|
end
|
52
59
|
end
|
@@ -87,10 +87,9 @@ describe ThinkingSphinx::ActiveRecord::Scopes do
|
|
87
87
|
search.by_foo('foo').search.options[:conditions].should == {:foo => 'foo', :name => 'foo'}
|
88
88
|
end
|
89
89
|
|
90
|
-
|
91
|
-
it "should apply the default scope options after other scope options to the underlying search object" do
|
90
|
+
it "should apply the default scope options before other scope options to the underlying search object" do
|
92
91
|
search = ThinkingSphinx::Search.new(:classes => [Alpha])
|
93
|
-
search.by_name('bar').search.options[:conditions].should == {:name => '
|
92
|
+
search.by_name('bar').search.options[:conditions].should == {:name => 'bar'}
|
94
93
|
end
|
95
94
|
end
|
96
95
|
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ThinkingSphinx::AbstractAdapter do
|
4
|
+
describe '.detect' do
|
5
|
+
let(:model) { stub('model') }
|
6
|
+
|
7
|
+
it "returns a MysqlAdapter object for :mysql" do
|
8
|
+
ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => :mysql)
|
9
|
+
|
10
|
+
adapter = ThinkingSphinx::AbstractAdapter.detect(model)
|
11
|
+
adapter.should be_a(ThinkingSphinx::MysqlAdapter)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns a PostgreSQLAdapter object for :postgresql" do
|
15
|
+
ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => :postgresql)
|
16
|
+
|
17
|
+
adapter = ThinkingSphinx::AbstractAdapter.detect(model)
|
18
|
+
adapter.should be_a(ThinkingSphinx::PostgreSQLAdapter)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "raises an exception for other responses" do
|
22
|
+
ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => :sqlite)
|
23
|
+
|
24
|
+
lambda {
|
25
|
+
ThinkingSphinx::AbstractAdapter.detect(model)
|
26
|
+
}.should raise_error
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '.adapter_for_model' do
|
31
|
+
let(:model) { stub('model') }
|
32
|
+
|
33
|
+
after :each do
|
34
|
+
ThinkingSphinx.database_adapter = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
it "translates strings to symbols" do
|
38
|
+
ThinkingSphinx.database_adapter = 'foo'
|
39
|
+
|
40
|
+
ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :foo
|
41
|
+
end
|
42
|
+
|
43
|
+
it "passes through symbols unchanged" do
|
44
|
+
ThinkingSphinx.database_adapter = :bar
|
45
|
+
|
46
|
+
ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :bar
|
47
|
+
end
|
48
|
+
|
49
|
+
it "returns standard_adapter_for_model if database_adapter is not set" do
|
50
|
+
ThinkingSphinx.database_adapter = nil
|
51
|
+
ThinkingSphinx::AbstractAdapter.stub!(:standard_adapter_for_model => :baz)
|
52
|
+
|
53
|
+
ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :baz
|
54
|
+
end
|
55
|
+
|
56
|
+
it "calls the lambda and returns it if one is provided" do
|
57
|
+
ThinkingSphinx.database_adapter = lambda { |model| :foo }
|
58
|
+
|
59
|
+
ThinkingSphinx::AbstractAdapter.adapter_for_model(model).should == :foo
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '.standard_adapter_for_model' do
|
64
|
+
let(:klass) { stub('connection class') }
|
65
|
+
let(:connection) { stub('connection', :class => klass) }
|
66
|
+
let(:model) { stub('model', :connection => connection) }
|
67
|
+
|
68
|
+
it "translates a normal MySQL adapter" do
|
69
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::MysqlAdapter')
|
70
|
+
|
71
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
72
|
+
should == :mysql
|
73
|
+
end
|
74
|
+
|
75
|
+
it "translates a MySQL plus adapter" do
|
76
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::MysqlplusAdapter')
|
77
|
+
|
78
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
79
|
+
should == :mysql
|
80
|
+
end
|
81
|
+
|
82
|
+
it "translates a MySQL2 adapter" do
|
83
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::Mysql2Adapter')
|
84
|
+
|
85
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
86
|
+
should == :mysql
|
87
|
+
end
|
88
|
+
|
89
|
+
it "translates a NullDB adapter to MySQL" do
|
90
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::NullDBAdapter')
|
91
|
+
|
92
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
93
|
+
should == :mysql
|
94
|
+
end
|
95
|
+
|
96
|
+
it "translates a normal PostgreSQL adapter" do
|
97
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter')
|
98
|
+
|
99
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
100
|
+
should == :postgresql
|
101
|
+
end
|
102
|
+
|
103
|
+
it "translates a JDBC MySQL adapter to MySQL" do
|
104
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter')
|
105
|
+
connection.stub(:config => {:adapter => 'jdbcmysql'})
|
106
|
+
|
107
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
108
|
+
should == :mysql
|
109
|
+
end
|
110
|
+
|
111
|
+
it "translates a JDBC PostgreSQL adapter to PostgreSQL" do
|
112
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter')
|
113
|
+
connection.stub(:config => {:adapter => 'jdbcpostgresql'})
|
114
|
+
|
115
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
116
|
+
should == :postgresql
|
117
|
+
end
|
118
|
+
|
119
|
+
it "returns other JDBC adapters without translation" do
|
120
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter')
|
121
|
+
connection.stub(:config => {:adapter => 'jdbcmssql'})
|
122
|
+
|
123
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
124
|
+
should == 'jdbcmssql'
|
125
|
+
end
|
126
|
+
|
127
|
+
it "returns other unknown adapters without translation" do
|
128
|
+
klass.stub(:name => 'ActiveRecord::ConnectionAdapters::FooAdapter')
|
129
|
+
|
130
|
+
ThinkingSphinx::AbstractAdapter.standard_adapter_for_model(model).
|
131
|
+
should == 'ActiveRecord::ConnectionAdapters::FooAdapter'
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|