thinking-sphinx 3.0.5 → 3.0.6
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 +3 -0
- data/HISTORY +25 -0
- data/README.textile +2 -2
- data/lib/thinking_sphinx.rb +5 -0
- data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +1 -1
- data/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +1 -1
- data/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb +1 -1
- data/lib/thinking_sphinx/active_record/property_sql_presenter.rb +10 -2
- data/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb +6 -7
- data/lib/thinking_sphinx/active_record/sql_builder/query.rb +8 -4
- data/lib/thinking_sphinx/active_record/sql_builder/statement.rb +7 -4
- data/lib/thinking_sphinx/active_record/sql_source.rb +1 -1
- data/lib/thinking_sphinx/configuration.rb +8 -3
- data/lib/thinking_sphinx/connection.rb +2 -0
- data/lib/thinking_sphinx/deletion.rb +1 -1
- data/lib/thinking_sphinx/deltas.rb +1 -1
- data/lib/thinking_sphinx/deltas/delete_job.rb +1 -1
- data/lib/thinking_sphinx/errors.rb +8 -0
- data/lib/thinking_sphinx/excerpter.rb +2 -2
- data/lib/thinking_sphinx/facet.rb +2 -2
- data/lib/thinking_sphinx/facet_search.rb +2 -1
- data/lib/thinking_sphinx/float_formatter.rb +33 -0
- data/lib/thinking_sphinx/index_set.rb +2 -4
- data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -3
- data/lib/thinking_sphinx/masks/scopes_mask.rb +5 -0
- data/lib/thinking_sphinx/masks/weight_enumerator_mask.rb +1 -1
- data/lib/thinking_sphinx/middlewares/active_record_translator.rb +6 -1
- data/lib/thinking_sphinx/middlewares/geographer.rb +10 -4
- data/lib/thinking_sphinx/middlewares/sphinxql.rb +14 -11
- data/lib/thinking_sphinx/middlewares/utf8.rb +2 -3
- data/lib/thinking_sphinx/panes/weight_pane.rb +1 -1
- data/lib/thinking_sphinx/rake_interface.rb +9 -7
- data/lib/thinking_sphinx/real_time/interpreter.rb +18 -0
- data/lib/thinking_sphinx/real_time/property.rb +4 -2
- data/lib/thinking_sphinx/search.rb +11 -10
- data/lib/thinking_sphinx/sphinxql.rb +17 -0
- data/lib/thinking_sphinx/tasks.rb +5 -1
- data/lib/thinking_sphinx/utf8.rb +16 -0
- data/spec/acceptance/attribute_access_spec.rb +4 -2
- data/spec/acceptance/searching_within_a_model_spec.rb +6 -0
- data/spec/acceptance/sorting_search_results_spec.rb +7 -0
- data/spec/acceptance/support/sphinx_controller.rb +5 -0
- data/spec/internal/app/indices/city_index.rb +1 -0
- data/spec/internal/app/indices/product_index.rb +1 -1
- data/spec/internal/tmp/.gitkeep +0 -0
- data/spec/thinking_sphinx/active_record/base_spec.rb +10 -0
- data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +3 -2
- data/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb +1 -1
- data/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb +2 -2
- data/spec/thinking_sphinx/active_record/field_spec.rb +13 -0
- data/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb +13 -0
- data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +62 -2
- data/spec/thinking_sphinx/configuration_spec.rb +1 -1
- data/spec/thinking_sphinx/deletion_spec.rb +4 -2
- data/spec/thinking_sphinx/deltas/default_delta_spec.rb +2 -1
- data/spec/thinking_sphinx/deltas_spec.rb +17 -6
- data/spec/thinking_sphinx/errors_spec.rb +7 -0
- data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
- data/spec/thinking_sphinx/masks/scopes_mask_spec.rb +64 -0
- data/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb +17 -1
- data/spec/thinking_sphinx/middlewares/geographer_spec.rb +11 -0
- data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +11 -0
- data/spec/thinking_sphinx/panes/weight_pane_spec.rb +1 -1
- data/spec/thinking_sphinx/rake_interface_spec.rb +6 -0
- data/spec/thinking_sphinx/real_time/field_spec.rb +13 -0
- data/spec/thinking_sphinx/real_time/interpreter_spec.rb +40 -0
- data/thinking-sphinx.gemspec +3 -2
- metadata +12 -8
- data/spec/internal/.gitignore +0 -1
@@ -9,19 +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[ThinkingSphinx::SphinxQL.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[ThinkingSphinx::SphinxQL.group_by]
|
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[
|
24
|
+
yield @search[index], row[ThinkingSphinx::SphinxQL.group_by],
|
25
|
+
row[ThinkingSphinx::SphinxQL.count]
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -19,6 +19,11 @@ class ThinkingSphinx::Masks::ScopesMask
|
|
19
19
|
ThinkingSphinx::Search::Merger.new(@search).merge! query, options
|
20
20
|
end
|
21
21
|
|
22
|
+
def search_for_ids(query = nil, options = {})
|
23
|
+
query, options = nil, query if query.is_a?(Hash)
|
24
|
+
search query, options.merge(:ids_only => true)
|
25
|
+
end
|
26
|
+
|
22
27
|
private
|
23
28
|
|
24
29
|
def apply_scope(scope, *args)
|
@@ -18,7 +18,12 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
|
|
18
18
|
|
19
19
|
def call
|
20
20
|
results_for_models # load now to avoid segfaults
|
21
|
-
|
21
|
+
|
22
|
+
context[:results] = if sql_options[:order]
|
23
|
+
results_for_models.values.first
|
24
|
+
else
|
25
|
+
context[:results].collect { |row| result_for(row) }
|
26
|
+
end
|
22
27
|
end
|
23
28
|
|
24
29
|
private
|
@@ -31,25 +31,30 @@ class ThinkingSphinx::Middlewares::Geographer <
|
|
31
31
|
|
32
32
|
delegate :geo, :latitude, :longitude, :to => :geolocation_attributes
|
33
33
|
|
34
|
+
def fixed_format(float)
|
35
|
+
ThinkingSphinx::FloatFormatter.new(float).fixed
|
36
|
+
end
|
37
|
+
|
34
38
|
def geolocation_attributes
|
35
39
|
@geolocation_attributes ||= GeolocationAttributes.new(context)
|
36
40
|
end
|
37
41
|
|
38
42
|
def geodist_clause
|
39
|
-
"GEODIST(#{geo.first}, #{geo.last}, #{latitude}, #{longitude}) AS geodist"
|
43
|
+
"GEODIST(#{fixed_format geo.first}, #{fixed_format geo.last}, #{latitude}, #{longitude}) AS geodist"
|
40
44
|
end
|
41
45
|
|
42
46
|
class GeolocationAttributes
|
47
|
+
attr_accessor :latitude, :longitude
|
48
|
+
|
43
49
|
def initialize(context)
|
44
|
-
self.context
|
45
|
-
self.latitude
|
50
|
+
self.context = context
|
51
|
+
self.latitude = latitude_attr if latitude_attr
|
46
52
|
self.longitude = longitude_attr if longitude_attr
|
47
53
|
end
|
48
54
|
|
49
55
|
def geo
|
50
56
|
search_context_options[:geo]
|
51
57
|
end
|
52
|
-
attr_accessor :latitude, :longitude
|
53
58
|
|
54
59
|
def latitude
|
55
60
|
@latitude ||= names.detect { |name| %w[lat latitude].include?(name) } || 'lat'
|
@@ -60,6 +65,7 @@ class ThinkingSphinx::Middlewares::Geographer <
|
|
60
65
|
end
|
61
66
|
|
62
67
|
private
|
68
|
+
|
63
69
|
attr_accessor :context
|
64
70
|
|
65
71
|
def latitude_attr
|
@@ -3,7 +3,8 @@ class ThinkingSphinx::Middlewares::SphinxQL <
|
|
3
3
|
|
4
4
|
SELECT_OPTIONS = [:ranker, :max_matches, :cutoff, :max_query_time,
|
5
5
|
:retry_count, :retry_delay, :field_weights, :index_weights, :reverse_scan,
|
6
|
-
:comment
|
6
|
+
:comment, :agent_query_timeout, :boolean_simplify, :global_idf, :idf,
|
7
|
+
:sort_method]
|
7
8
|
|
8
9
|
def call(contexts)
|
9
10
|
contexts.each do |context|
|
@@ -29,6 +30,10 @@ class ThinkingSphinx::Middlewares::SphinxQL <
|
|
29
30
|
|
30
31
|
attr_reader :context
|
31
32
|
|
33
|
+
delegate :search, :configuration, :to => :context
|
34
|
+
delegate :options, :to => :search
|
35
|
+
delegate :settings, :to => :configuration
|
36
|
+
|
32
37
|
def classes
|
33
38
|
options[:classes] || []
|
34
39
|
end
|
@@ -75,9 +80,9 @@ class ThinkingSphinx::Middlewares::SphinxQL <
|
|
75
80
|
end
|
76
81
|
|
77
82
|
def indices_match_classes?
|
78
|
-
indices.collect(&:reference).uniq == classes.collect { |klass|
|
83
|
+
indices.collect(&:reference).uniq.sort == classes.collect { |klass|
|
79
84
|
klass.name.underscore.to_sym
|
80
|
-
}
|
85
|
+
}.sort
|
81
86
|
end
|
82
87
|
|
83
88
|
def inheritance_column_select(klass)
|
@@ -127,12 +132,13 @@ SQL
|
|
127
132
|
end
|
128
133
|
|
129
134
|
def indices
|
130
|
-
@indices ||=
|
135
|
+
@indices ||= begin
|
136
|
+
set = ThinkingSphinx::IndexSet.new classes, options[:indices]
|
137
|
+
raise ThinkingSphinx::NoIndicesError if set.empty?
|
138
|
+
set
|
139
|
+
end
|
131
140
|
end
|
132
141
|
|
133
|
-
delegate :search, :to => :context
|
134
|
-
delegate :options, :to => :search
|
135
|
-
|
136
142
|
def order_clause
|
137
143
|
order_by = options[:order]
|
138
144
|
order_by = "#{order_by} ASC" if order_by.is_a? Symbol
|
@@ -148,11 +154,8 @@ SQL
|
|
148
154
|
end
|
149
155
|
end
|
150
156
|
|
151
|
-
delegate :configuration, :to => :context
|
152
|
-
delegate :settings, :to => :configuration
|
153
|
-
|
154
157
|
def values
|
155
|
-
options[:select] ||=
|
158
|
+
options[:select] ||= "*, #{ThinkingSphinx::SphinxQL.group_by}, #{ThinkingSphinx::SphinxQL.count}" if group_attribute.present?
|
156
159
|
options[:select]
|
157
160
|
end
|
158
161
|
|
@@ -5,7 +5,7 @@ class ThinkingSphinx::Middlewares::UTF8 <
|
|
5
5
|
contexts.each do |context|
|
6
6
|
context[:results].each { |row| update_row row }
|
7
7
|
update_row context[:meta]
|
8
|
-
end
|
8
|
+
end unless ThinkingSphinx::Configuration.instance.settings['utf8']
|
9
9
|
|
10
10
|
app.call contexts
|
11
11
|
end
|
@@ -16,8 +16,7 @@ class ThinkingSphinx::Middlewares::UTF8 <
|
|
16
16
|
row.each do |key, value|
|
17
17
|
next unless value.is_a?(String)
|
18
18
|
|
19
|
-
|
20
|
-
row[key] = value.force_encoding("UTF-8")
|
19
|
+
row[key] = ThinkingSphinx::UTF8.encode value
|
21
20
|
end
|
22
21
|
end
|
23
22
|
end
|
@@ -14,22 +14,24 @@ class ThinkingSphinx::RakeInterface
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def generate
|
17
|
-
configuration.preload_indices
|
18
|
-
configuration.render
|
19
|
-
|
20
|
-
FileUtils.mkdir_p configuration.indices_location
|
21
|
-
|
22
17
|
indices = configuration.indices.select { |index| index.type == 'rt' }
|
23
18
|
indices.each do |index|
|
24
19
|
ThinkingSphinx::RealTime::Populator.populate index
|
25
20
|
end
|
26
21
|
end
|
27
22
|
|
28
|
-
def index(reconfigure = true)
|
23
|
+
def index(reconfigure = true, verbose = true)
|
29
24
|
configure if reconfigure
|
30
25
|
FileUtils.mkdir_p configuration.indices_location
|
31
26
|
ThinkingSphinx.before_index_hooks.each { |hook| hook.call }
|
32
|
-
controller.index :verbose =>
|
27
|
+
controller.index :verbose => verbose
|
28
|
+
end
|
29
|
+
|
30
|
+
def prepare
|
31
|
+
configuration.preload_indices
|
32
|
+
configuration.render
|
33
|
+
|
34
|
+
FileUtils.mkdir_p configuration.indices_location
|
33
35
|
end
|
34
36
|
|
35
37
|
def start
|
@@ -13,6 +13,8 @@ class ThinkingSphinx::RealTime::Interpreter <
|
|
13
13
|
@index.fields += columns.collect { |column|
|
14
14
|
::ThinkingSphinx::RealTime::Field.new column, options
|
15
15
|
}
|
16
|
+
|
17
|
+
append_sortable_attributes columns, options if options[:sortable]
|
16
18
|
end
|
17
19
|
|
18
20
|
def scope(&block)
|
@@ -28,4 +30,20 @@ class ThinkingSphinx::RealTime::Interpreter <
|
|
28
30
|
def where(condition)
|
29
31
|
@index.conditions << condition
|
30
32
|
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def append_sortable_attributes(columns, options)
|
37
|
+
options = options.except(:sortable).merge(:type => :string)
|
38
|
+
|
39
|
+
@index.attributes += columns.collect { |column|
|
40
|
+
aliased_name = options[:as]
|
41
|
+
aliased_name ||= column.__name.to_sym if column.respond_to?(:__name)
|
42
|
+
aliased_name ||= column
|
43
|
+
|
44
|
+
options[:as] = "#{aliased_name}_sort".to_sym
|
45
|
+
|
46
|
+
::ThinkingSphinx::RealTime::Attribute.new column, options
|
47
|
+
}
|
48
|
+
end
|
31
49
|
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
class ThinkingSphinx::RealTime::Property
|
2
2
|
include ThinkingSphinx::Core::Property
|
3
3
|
|
4
|
-
attr_reader :options
|
4
|
+
attr_reader :column, :options
|
5
5
|
|
6
6
|
def initialize(column, options = {})
|
7
|
-
@
|
7
|
+
@options = options
|
8
|
+
@column = column.respond_to?(:__name) ? column :
|
9
|
+
ThinkingSphinx::ActiveRecord::Column.new(column)
|
8
10
|
end
|
9
11
|
|
10
12
|
def name
|
@@ -18,14 +18,12 @@ class ThinkingSphinx::Search < Array
|
|
18
18
|
undef_method method
|
19
19
|
}
|
20
20
|
|
21
|
-
attr_reader :options
|
21
|
+
attr_reader :options
|
22
22
|
attr_accessor :query
|
23
23
|
|
24
24
|
def initialize(query = nil, options = {})
|
25
25
|
query, options = nil, query if query.is_a?(Hash)
|
26
26
|
@query, @options = query, options
|
27
|
-
@masks = @options.delete(:masks) || DEFAULT_MASKS.clone
|
28
|
-
@middleware = @options.delete(:middleware)
|
29
27
|
|
30
28
|
populate if options[:populate]
|
31
29
|
end
|
@@ -40,6 +38,10 @@ class ThinkingSphinx::Search < Array
|
|
40
38
|
options[:page].to_i
|
41
39
|
end
|
42
40
|
|
41
|
+
def masks
|
42
|
+
@masks ||= @options[:masks] || DEFAULT_MASKS.clone
|
43
|
+
end
|
44
|
+
|
43
45
|
def meta
|
44
46
|
populate
|
45
47
|
context[:meta]
|
@@ -94,6 +96,11 @@ class ThinkingSphinx::Search < Array
|
|
94
96
|
|
95
97
|
private
|
96
98
|
|
99
|
+
def default_middleware
|
100
|
+
options[:ids_only] ? ThinkingSphinx::Middlewares::IDS_ONLY :
|
101
|
+
ThinkingSphinx::Middlewares::DEFAULT
|
102
|
+
end
|
103
|
+
|
97
104
|
def mask_stack
|
98
105
|
@mask_stack ||= masks.collect { |klass| klass.new self }
|
99
106
|
end
|
@@ -109,13 +116,7 @@ class ThinkingSphinx::Search < Array
|
|
109
116
|
end
|
110
117
|
|
111
118
|
def middleware
|
112
|
-
@middleware
|
113
|
-
if options[:ids_only]
|
114
|
-
ThinkingSphinx::Middlewares::IDS_ONLY
|
115
|
-
else
|
116
|
-
ThinkingSphinx::Middlewares::DEFAULT
|
117
|
-
end
|
118
|
-
end
|
119
|
+
@options[:middleware] || default_middleware
|
119
120
|
end
|
120
121
|
end
|
121
122
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ThinkingSphinx::SphinxQL
|
2
|
+
mattr_accessor :weight, :group_by, :count
|
3
|
+
|
4
|
+
def self.functions!
|
5
|
+
self.weight = 'weight()'
|
6
|
+
self.group_by = 'groupby()'
|
7
|
+
self.count = 'count(*)'
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.variables!
|
11
|
+
self.weight = '@weight'
|
12
|
+
self.group_by = '@groupby'
|
13
|
+
self.count = '@count'
|
14
|
+
end
|
15
|
+
|
16
|
+
self.variables!
|
17
|
+
end
|
@@ -6,7 +6,10 @@ namespace :ts do
|
|
6
6
|
|
7
7
|
desc 'Generate the Sphinx configuration file and process all indices'
|
8
8
|
task :index => :environment do
|
9
|
-
interface.index(
|
9
|
+
interface.index(
|
10
|
+
ENV['INDEX_ONLY'] != 'true',
|
11
|
+
!Rake.application.options.silent
|
12
|
+
)
|
10
13
|
end
|
11
14
|
|
12
15
|
desc 'Clear out Sphinx files'
|
@@ -16,6 +19,7 @@ namespace :ts do
|
|
16
19
|
|
17
20
|
desc 'Generate fresh index files for real-time indices'
|
18
21
|
task :generate => :environment do
|
22
|
+
interface.prepare
|
19
23
|
interface.generate
|
20
24
|
end
|
21
25
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class ThinkingSphinx::UTF8
|
2
|
+
attr_reader :string
|
3
|
+
|
4
|
+
def self.encode(string)
|
5
|
+
new(string).encode
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(string)
|
9
|
+
@string = string
|
10
|
+
end
|
11
|
+
|
12
|
+
def encode
|
13
|
+
string.encode!('ISO-8859-1')
|
14
|
+
string.force_encoding('UTF-8')
|
15
|
+
end
|
16
|
+
end
|
@@ -15,7 +15,8 @@ describe 'Accessing attributes directly via search results', :live => true do
|
|
15
15
|
Book.create! :title => 'American Gods', :year => 2001
|
16
16
|
index
|
17
17
|
|
18
|
-
search = Book.search
|
18
|
+
search = Book.search 'gods',
|
19
|
+
:select => "*, #{ThinkingSphinx::SphinxQL.weight}"
|
19
20
|
search.context[:panes] << ThinkingSphinx::Panes::WeightPane
|
20
21
|
|
21
22
|
search.first.weight.should == 2500
|
@@ -25,7 +26,8 @@ describe 'Accessing attributes directly via search results', :live => true do
|
|
25
26
|
gods = Book.create! :title => 'American Gods', :year => 2001
|
26
27
|
index
|
27
28
|
|
28
|
-
search = Book.search
|
29
|
+
search = Book.search 'gods',
|
30
|
+
:select => "*, #{ThinkingSphinx::SphinxQL.weight}"
|
29
31
|
search.masks << ThinkingSphinx::Masks::WeightEnumeratorMask
|
30
32
|
|
31
33
|
expectations = [[gods, 2500]]
|
@@ -65,6 +65,12 @@ describe 'Searching within a model', :live => true do
|
|
65
65
|
User.recent.search
|
66
66
|
}.should raise_error(ThinkingSphinx::MixedScopesError)
|
67
67
|
end
|
68
|
+
|
69
|
+
it "raises an error if the model has no indices defined" do
|
70
|
+
lambda {
|
71
|
+
Category.search.to_a
|
72
|
+
}.should raise_error(ThinkingSphinx::NoIndicesError)
|
73
|
+
end
|
68
74
|
end
|
69
75
|
|
70
76
|
describe 'Searching within a model with a realtime index', :live => true do
|
@@ -28,6 +28,13 @@ describe 'Sorting search results', :live => true do
|
|
28
28
|
Book.search(:order => :title).to_a.should == [gods, boys, grave]
|
29
29
|
end
|
30
30
|
|
31
|
+
it "sorts by a given sortable field with real-time indices" do
|
32
|
+
widgets = Product.create! :name => 'Widgets'
|
33
|
+
gadgets = Product.create! :name => 'Gadgets'
|
34
|
+
|
35
|
+
Product.search(:order => "name_sort ASC").to_a.should == [gadgets, widgets]
|
36
|
+
end
|
37
|
+
|
31
38
|
it "can sort with a provided expression" do
|
32
39
|
gods = Book.create! :title => 'American Gods', :year => 2001
|
33
40
|
grave = Book.create! :title => 'The Graveyard Book', :year => 2009
|
@@ -9,6 +9,11 @@ class SphinxController
|
|
9
9
|
|
10
10
|
ThinkingSphinx::Configuration.reset
|
11
11
|
|
12
|
+
if ENV['SPHINX_VERSION'].try :[], /2.1.\d/
|
13
|
+
ThinkingSphinx::SphinxQL.functions!
|
14
|
+
ThinkingSphinx::Configuration.instance.settings['utf8'] = true
|
15
|
+
end
|
16
|
+
|
12
17
|
ActiveSupport::Dependencies.loaded.each do |path|
|
13
18
|
$LOADED_FEATURES.delete "#{path}.rb"
|
14
19
|
end
|