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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/HISTORY +25 -0
  4. data/README.textile +2 -2
  5. data/lib/thinking_sphinx.rb +5 -0
  6. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +1 -1
  7. data/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +1 -1
  8. data/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb +1 -1
  9. data/lib/thinking_sphinx/active_record/property_sql_presenter.rb +10 -2
  10. data/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb +6 -7
  11. data/lib/thinking_sphinx/active_record/sql_builder/query.rb +8 -4
  12. data/lib/thinking_sphinx/active_record/sql_builder/statement.rb +7 -4
  13. data/lib/thinking_sphinx/active_record/sql_source.rb +1 -1
  14. data/lib/thinking_sphinx/configuration.rb +8 -3
  15. data/lib/thinking_sphinx/connection.rb +2 -0
  16. data/lib/thinking_sphinx/deletion.rb +1 -1
  17. data/lib/thinking_sphinx/deltas.rb +1 -1
  18. data/lib/thinking_sphinx/deltas/delete_job.rb +1 -1
  19. data/lib/thinking_sphinx/errors.rb +8 -0
  20. data/lib/thinking_sphinx/excerpter.rb +2 -2
  21. data/lib/thinking_sphinx/facet.rb +2 -2
  22. data/lib/thinking_sphinx/facet_search.rb +2 -1
  23. data/lib/thinking_sphinx/float_formatter.rb +33 -0
  24. data/lib/thinking_sphinx/index_set.rb +2 -4
  25. data/lib/thinking_sphinx/masks/group_enumerators_mask.rb +4 -3
  26. data/lib/thinking_sphinx/masks/scopes_mask.rb +5 -0
  27. data/lib/thinking_sphinx/masks/weight_enumerator_mask.rb +1 -1
  28. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +6 -1
  29. data/lib/thinking_sphinx/middlewares/geographer.rb +10 -4
  30. data/lib/thinking_sphinx/middlewares/sphinxql.rb +14 -11
  31. data/lib/thinking_sphinx/middlewares/utf8.rb +2 -3
  32. data/lib/thinking_sphinx/panes/weight_pane.rb +1 -1
  33. data/lib/thinking_sphinx/rake_interface.rb +9 -7
  34. data/lib/thinking_sphinx/real_time/interpreter.rb +18 -0
  35. data/lib/thinking_sphinx/real_time/property.rb +4 -2
  36. data/lib/thinking_sphinx/search.rb +11 -10
  37. data/lib/thinking_sphinx/sphinxql.rb +17 -0
  38. data/lib/thinking_sphinx/tasks.rb +5 -1
  39. data/lib/thinking_sphinx/utf8.rb +16 -0
  40. data/spec/acceptance/attribute_access_spec.rb +4 -2
  41. data/spec/acceptance/searching_within_a_model_spec.rb +6 -0
  42. data/spec/acceptance/sorting_search_results_spec.rb +7 -0
  43. data/spec/acceptance/support/sphinx_controller.rb +5 -0
  44. data/spec/internal/app/indices/city_index.rb +1 -0
  45. data/spec/internal/app/indices/product_index.rb +1 -1
  46. data/spec/internal/tmp/.gitkeep +0 -0
  47. data/spec/thinking_sphinx/active_record/base_spec.rb +10 -0
  48. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +3 -2
  49. data/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb +1 -1
  50. data/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb +2 -2
  51. data/spec/thinking_sphinx/active_record/field_spec.rb +13 -0
  52. data/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb +13 -0
  53. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +62 -2
  54. data/spec/thinking_sphinx/configuration_spec.rb +1 -1
  55. data/spec/thinking_sphinx/deletion_spec.rb +4 -2
  56. data/spec/thinking_sphinx/deltas/default_delta_spec.rb +2 -1
  57. data/spec/thinking_sphinx/deltas_spec.rb +17 -6
  58. data/spec/thinking_sphinx/errors_spec.rb +7 -0
  59. data/spec/thinking_sphinx/facet_search_spec.rb +6 -6
  60. data/spec/thinking_sphinx/masks/scopes_mask_spec.rb +64 -0
  61. data/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb +17 -1
  62. data/spec/thinking_sphinx/middlewares/geographer_spec.rb +11 -0
  63. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +11 -0
  64. data/spec/thinking_sphinx/panes/weight_pane_spec.rb +1 -1
  65. data/spec/thinking_sphinx/rake_interface_spec.rb +6 -0
  66. data/spec/thinking_sphinx/real_time/field_spec.rb +13 -0
  67. data/spec/thinking_sphinx/real_time/interpreter_spec.rb +40 -0
  68. data/thinking-sphinx.gemspec +3 -2
  69. metadata +12 -8
  70. 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['@count']
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['@groupby']
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['@groupby'], row['@count']
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)
@@ -9,7 +9,7 @@ class ThinkingSphinx::Masks::WeightEnumeratorMask
9
9
 
10
10
  def each_with_weight(&block)
11
11
  @search.raw.each_with_index do |row, index|
12
- yield @search[index], row['@weight']
12
+ yield @search[index], row[ThinkingSphinx::SphinxQL.weight]
13
13
  end
14
14
  end
15
15
  end
@@ -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
- context[:results] = context[:results].collect { |row| result_for(row) }
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 = context
45
- self.latitude = latitude_attr if latitude_attr
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 ||= ThinkingSphinx::IndexSet.new classes, options[: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] ||= '*, @groupby, @count' if group_attribute.present?
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
- value.encode!("ISO-8859-1")
20
- row[key] = value.force_encoding("UTF-8")
19
+ row[key] = ThinkingSphinx::UTF8.encode value
21
20
  end
22
21
  end
23
22
  end
@@ -4,6 +4,6 @@ class ThinkingSphinx::Panes::WeightPane
4
4
  end
5
5
 
6
6
  def weight
7
- @raw['@weight']
7
+ @raw[ThinkingSphinx::SphinxQL.weight]
8
8
  end
9
9
  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 => true
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
- @column, @options = column, options
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, :masks
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 ||= begin
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(ENV['INDEX_ONLY'] != 'true')
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('gods', :select => '*, @weight')
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('gods', :select => '*, @weight')
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
@@ -3,4 +3,5 @@ ThinkingSphinx::Index.define :city, :with => :active_record do
3
3
  has lat, lng
4
4
 
5
5
  set_property :charset_table => '0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F, U+0130'
6
+ set_property :utf8? => true
6
7
  end