thinking-sphinx 3.0.5 → 3.0.6

Sign up to get free protection for your applications and to get access to all the features.
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