thinking-sphinx 3.0.2 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/Appraisals +1 -1
  2. data/HISTORY +17 -0
  3. data/README.textile +1 -1
  4. data/gemfiles/rails_4_0.gemfile +1 -1
  5. data/lib/thinking_sphinx.rb +6 -0
  6. data/lib/thinking_sphinx/active_record/association_proxy.rb +6 -0
  7. data/lib/thinking_sphinx/active_record/associations.rb +4 -1
  8. data/lib/thinking_sphinx/active_record/base.rb +6 -0
  9. data/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +4 -0
  10. data/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb +9 -1
  11. data/lib/thinking_sphinx/active_record/property_sql_presenter.rb +14 -1
  12. data/lib/thinking_sphinx/active_record/sql_source.rb +9 -5
  13. data/lib/thinking_sphinx/capistrano.rb +1 -0
  14. data/lib/thinking_sphinx/core/index.rb +7 -7
  15. data/lib/thinking_sphinx/deltas.rb +3 -1
  16. data/lib/thinking_sphinx/deltas/default_delta.rb +4 -7
  17. data/lib/thinking_sphinx/deltas/delete_job.rb +15 -0
  18. data/lib/thinking_sphinx/deltas/index_job.rb +16 -0
  19. data/lib/thinking_sphinx/errors.rb +3 -0
  20. data/lib/thinking_sphinx/excerpter.rb +8 -2
  21. data/lib/thinking_sphinx/masks/pagination_mask.rb +3 -8
  22. data/lib/thinking_sphinx/middlewares.rb +3 -0
  23. data/lib/thinking_sphinx/middlewares/sphinxql.rb +0 -4
  24. data/lib/thinking_sphinx/middlewares/utf8.rb +23 -0
  25. data/lib/thinking_sphinx/rake_interface.rb +1 -0
  26. data/lib/thinking_sphinx/real_time/index.rb +8 -0
  27. data/lib/thinking_sphinx/real_time/index/template.rb +2 -2
  28. data/lib/thinking_sphinx/search.rb +8 -2
  29. data/lib/thinking_sphinx/search/context.rb +1 -1
  30. data/lib/thinking_sphinx/search/merger.rb +2 -0
  31. data/spec/acceptance/excerpts_spec.rb +13 -0
  32. data/spec/acceptance/facets_spec.rb +4 -1
  33. data/spec/acceptance/index_options_spec.rb +40 -0
  34. data/spec/acceptance/searching_within_a_model_spec.rb +18 -0
  35. data/spec/internal/app/models/city.rb +1 -0
  36. data/spec/internal/app/models/user.rb +4 -1
  37. data/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb +6 -0
  38. data/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb +14 -1
  39. data/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb +17 -2
  40. data/spec/thinking_sphinx/configuration_spec.rb +3 -0
  41. data/spec/thinking_sphinx/deltas/default_delta_spec.rb +3 -1
  42. data/spec/thinking_sphinx/masks/pagination_mask_spec.rb +6 -6
  43. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +0 -11
  44. data/spec/thinking_sphinx/rake_interface_spec.rb +10 -0
  45. data/thinking-sphinx.gemspec +2 -2
  46. metadata +48 -19
  47. checksums.yaml +0 -7
data/Appraisals CHANGED
@@ -7,5 +7,5 @@ appraise 'rails_3_2' do
7
7
  end
8
8
 
9
9
  appraise 'rails_4_0' do
10
- gem 'rails', '~> 4.0.0.beta1'
10
+ gem 'rails', '~> 4.0.0.rc1'
11
11
  end
data/HISTORY CHANGED
@@ -1,3 +1,20 @@
1
+ 2013-05-07: 3.0.3
2
+ * [CHANGE] Updating Riddle dependency to be >= 1.5.6
3
+ * [FEATURE] INDEX_ONLY environment flag is passed through when invoked through Capistrano (Demian Ferreiro).
4
+ * [FEATURE] use_64_bit option returns as cast_to_timestamp instead (Denis Abushaev).
5
+ * [FIX] Update to association handling for Rails/ActiveRecord 4.0.0.rc1.
6
+ * [CHANGE] Delta jobs get common classes to allow third-party delta behaviours to leverage Thinking Sphinx.
7
+ * [FEATURE] Collection of hooks (lambdas) that get called before indexing. Useful for delta libraries.
8
+ * [FIX] Cast and concatenate multi-column attributes correctly.
9
+ * [FIX] Don't load fields or attributes when building a real-time index - otherwise the index is translated before it has a chance to be built.
10
+ * [CHANGE] Raise ThinkingSphinx::MixedScopesError if a search is called through an ActiveRecord scope.
11
+ * [FIX] Default search panes are cloned for each search.
12
+ * [FIX] Index-level settings (via set_property) are now applied consistently after global settings (in thinking_sphinx.yml).
13
+ * [FIX] All string values returned from Sphinx are now properly converted to UTF8.
14
+ * [CHANGE] GroupEnumeratorsMask is now a default mask, as masks need to be in place before search results are populated/the middleware is called (and previously it was being added within a middleware call).
15
+ * [FIX] The default search masks are now cloned for each search, instead of referring to the constant (and potentially modifying it often).
16
+ * [CHANGE] The current_page method is now a part of ThinkingSphinx::Search, as it is used when populating results.
17
+
1
18
  2013-03-23: 3.0.2
2
19
  * [CHANGE] per_page now accepts an optional paging limit, to match WillPaginate's behaviour. If none is supplied, it just returns the page size.
3
20
  * [FEATURE] Ruby 2.0 support.
@@ -75,7 +75,7 @@ end</code></pre>
75
75
  :skip_sti => true,
76
76
  :classes => [User, AdminUser, SupportUser]</code></pre>
77
77
 
78
- * The option @:rank_mode@ has now become @:ranker@ - and the options (as strings or symbols) are as follows: proximity_bm25, bm25, none, wordcount, proximity, matchany, and fieldmask.
78
+ * The option @:rank_mode@ has now become @:ranker@ - and the options (as strings or symbols) are as follows: proximity_bm25, bm25, none, wordcount, proximity, matchany, fieldmask, sph04 and expr.
79
79
  * There are no explicit sorting modes - all sorting must be on attributes followed by ASC or DESC. For example: <code>:order => '@weight DESC, created_at ASC'</code>.
80
80
  * If you specify just an attribute name as a symbol for the @:order@ option, it will be given the ascending direction by default. So, @:order => :created_at@ is equivalent to @:order => 'created_at ASC'@.
81
81
  * If you want to use a calculated expression for sorting, you must specify the expression as a new attribute, then use that attribute in your @:order@ option. This is done using the @:select@ option to specify extra columns available in the underlying SphinxQL (_not_ ActiveRecord/SQL) query.
@@ -6,6 +6,6 @@ gem "mysql2", "~> 0.3.12b4", :platform=>:ruby
6
6
  gem "pg", "~> 0.11.0", :platform=>:ruby
7
7
  gem "activerecord-jdbcmysql-adapter", "~> 1.1.3", :platform=>:jruby
8
8
  gem "activerecord-jdbcpostgresql-adapter", "~> 1.1.3", :platform=>:jruby
9
- gem "rails", "~> 4.0.0.beta1"
9
+ gem "rails", "~> 4.0.0.rc1"
10
10
 
11
11
  gemspec :path=>"../"
@@ -28,6 +28,12 @@ module ThinkingSphinx
28
28
  search = ThinkingSphinx::Search.new query, options
29
29
  ThinkingSphinx::Search::Merger.new(search).merge! nil, :ids_only => true
30
30
  end
31
+
32
+ def self.before_index_hooks
33
+ @before_index_hooks
34
+ end
35
+
36
+ @before_index_hooks = []
31
37
  end
32
38
 
33
39
  # Core
@@ -2,11 +2,17 @@ module ThinkingSphinx::ActiveRecord::AssociationProxy
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def search(query = nil, options = {})
5
+ query, options = nil, query if query.is_a?(Hash)
6
+ options[:ignore_scopes] = true
7
+
5
8
  ThinkingSphinx::Search::Merger.new(super).merge! nil,
6
9
  :with => association_filter
7
10
  end
8
11
 
9
12
  def search_for_ids(query = nil, options = {})
13
+ query, options = nil, query if query.is_a?(Hash)
14
+ options[:ignore_scopes] = true
15
+
10
16
  ThinkingSphinx::Search::Merger.new(super).merge! nil,
11
17
  :with => association_filter
12
18
  end
@@ -75,7 +75,10 @@ class ThinkingSphinx::ActiveRecord::Associations
75
75
  end
76
76
 
77
77
  def reflection_for(stack)
78
- parent_for(stack).active_record.reflections[stack.last]
78
+ parent = parent_for(stack)
79
+ klass = parent.respond_to?(:base_klass) ? parent.base_klass :
80
+ parent.active_record
81
+ klass.reflections[stack.last]
79
82
  end
80
83
 
81
84
  def rewrite_conditions_for(join)
@@ -24,6 +24,12 @@ module ThinkingSphinx::ActiveRecord::Base
24
24
 
25
25
  merger.merge! *default_sphinx_scope_response if default_sphinx_scope?
26
26
  merger.merge! query, options
27
+
28
+ if current_scope && !merger.search.options[:ignore_scopes]
29
+ raise ThinkingSphinx::MixedScopesError,
30
+ 'You cannot search with Sphinx through ActiveRecord scopes'
31
+ end
32
+
27
33
  merger.merge! nil, :classes => [self]
28
34
  end
29
35
 
@@ -5,6 +5,10 @@ class ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter <
5
5
  value ? 1 : 0
6
6
  end
7
7
 
8
+ def cast_to_string(clause)
9
+ "CAST(#{clause} AS char)"
10
+ end
11
+
8
12
  def cast_to_timestamp(clause)
9
13
  "UNIX_TIMESTAMP(#{clause})"
10
14
  end
@@ -5,8 +5,16 @@ class ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter <
5
5
  value ? 'TRUE' : 'FALSE'
6
6
  end
7
7
 
8
+ def cast_to_string(clause)
9
+ "#{clause}::varchar"
10
+ end
11
+
8
12
  def cast_to_timestamp(clause)
9
- "extract(epoch from #{clause})::int"
13
+ if ThinkingSphinx::Configuration.instance.settings['64bit_timestamps']
14
+ "extract(epoch from #{clause})::bigint"
15
+ else
16
+ "extract(epoch from #{clause})::int"
17
+ end
10
18
  end
11
19
 
12
20
  def concatenate(clause, separator = ' ')
@@ -30,7 +30,7 @@ class ThinkingSphinx::ActiveRecord::PropertySQLPresenter
30
30
  def casted_column_with_table
31
31
  clause = columns_with_table
32
32
  clause = adapter.cast_to_timestamp(clause) if property.type == :timestamp
33
- clause = adapter.concatenate(clause, ' ') if concatenating?
33
+ clause = concatenate clause
34
34
  if aggregate?
35
35
  clause = adapter.group_concatenate(clause, aggregate_separator)
36
36
  end
@@ -60,6 +60,19 @@ class ThinkingSphinx::ActiveRecord::PropertySQLPresenter
60
60
  property.columns.length > 1
61
61
  end
62
62
 
63
+ def concatenate(clause)
64
+ return clause unless concatenating?
65
+
66
+ if property.type.nil?
67
+ adapter.concatenate clause, ' '
68
+ else
69
+ clause = clause.split(', ').collect { |part|
70
+ adapter.cast_to_string part
71
+ }.join(', ')
72
+ adapter.concatenate clause, ','
73
+ end
74
+ end
75
+
63
76
  def group?
64
77
  !(aggregate? || property.columns.any?(&:string?))
65
78
  end
@@ -25,6 +25,8 @@ class ThinkingSphinx::ActiveRecord::SQLSource < Riddle::Configuration::SQLSource
25
25
  name = "#{options[:name] || model.name.downcase}_#{options[:position]}"
26
26
 
27
27
  super name, type
28
+
29
+ apply_defaults
28
30
  end
29
31
 
30
32
  def adapter
@@ -57,11 +59,6 @@ class ThinkingSphinx::ActiveRecord::SQLSource < Riddle::Configuration::SQLSource
57
59
  end
58
60
 
59
61
  def render
60
- self.class.settings.each do |setting|
61
- value = config.settings[setting.to_s]
62
- send("#{setting}=", value) unless value.nil?
63
- end
64
-
65
62
  prepare_for_render unless @prepared
66
63
 
67
64
  super
@@ -80,6 +77,13 @@ class ThinkingSphinx::ActiveRecord::SQLSource < Riddle::Configuration::SQLSource
80
77
 
81
78
  private
82
79
 
80
+ def apply_defaults
81
+ self.class.settings.each do |setting|
82
+ value = config.settings[setting.to_s]
83
+ send("#{setting}=", value) unless value.nil?
84
+ end
85
+ end
86
+
83
87
  def attribute_array_for(type)
84
88
  instance_variable_get "@sql_attr_#{type}".to_sym
85
89
  end
@@ -56,6 +56,7 @@ if you alter the structure of your indexes.
56
56
  def rake(tasks)
57
57
  rails_env = fetch(:rails_env, 'production')
58
58
  rake = fetch(:rake, 'rake')
59
+ tasks += ' INDEX_ONLY=true' if ENV['INDEX_ONLY'] == 'true'
59
60
 
60
61
  run "if [ -d #{release_path} ]; then cd #{release_path}; else cd #{current_path}; fi; if [ -f Rakefile ]; then #{rake} RAILS_ENV=#{rails_env} #{tasks}; fi;"
61
62
  end
@@ -25,10 +25,15 @@ module ThinkingSphinx::Core::Index
25
25
  end
26
26
 
27
27
  def interpret_definition!
28
- return if @interpreted_definition || @definition_block.nil?
28
+ return if @interpreted_definition
29
+
30
+ self.class.settings.each do |setting|
31
+ value = config.settings[setting.to_s]
32
+ send("#{setting}=", value) unless value.nil?
33
+ end
29
34
 
30
35
  @interpreted_definition = true
31
- interpreter.translate! self, @definition_block
36
+ interpreter.translate! self, @definition_block if @definition_block
32
37
  end
33
38
 
34
39
  def model
@@ -59,11 +64,6 @@ module ThinkingSphinx::Core::Index
59
64
  end
60
65
 
61
66
  def pre_render
62
- self.class.settings.each do |setting|
63
- value = config.settings[setting.to_s]
64
- send("#{setting}=", value) unless value.nil?
65
- end
66
-
67
67
  interpret_definition!
68
68
  end
69
69
  end
@@ -2,7 +2,7 @@ module ThinkingSphinx::Deltas
2
2
  def self.config
3
3
  ThinkingSphinx::Configuration.instance
4
4
  end
5
-
5
+
6
6
  def self.processor_for(delta)
7
7
  case delta
8
8
  when TrueClass
@@ -40,3 +40,5 @@ module ThinkingSphinx::Deltas
40
40
  end
41
41
 
42
42
  require 'thinking_sphinx/deltas/default_delta'
43
+ require 'thinking_sphinx/deltas/delete_job'
44
+ require 'thinking_sphinx/deltas/index_job'
@@ -10,16 +10,13 @@ class ThinkingSphinx::Deltas::DefaultDelta
10
10
  end
11
11
 
12
12
  def delete(index, instance)
13
- ThinkingSphinx::Connection.new.execute Riddle::Query.update(
14
- index.name, index.document_id_for_key(instance.id),
15
- :sphinx_deleted => true
16
- )
17
- rescue Mysql2::Error => error
18
- # This isn't vital, so don't raise the error.
13
+ ThinkingSphinx::Deltas::DeleteJob.new(
14
+ index.name, index.document_id_for_key(instance.id)
15
+ ).perform
19
16
  end
20
17
 
21
18
  def index(index)
22
- controller.index index.name, :verbose => !config.settings['quiet_deltas']
19
+ ThinkingSphinx::Deltas::IndexJob.new(index.name).perform
23
20
  end
24
21
 
25
22
  def reset_query
@@ -0,0 +1,15 @@
1
+ class ThinkingSphinx::Deltas::DeleteJob
2
+ def initialize(index_name, document_id)
3
+ @index_name, @document_id = index_name, document_id
4
+ end
5
+
6
+ def perform
7
+ ThinkingSphinx::Connection.pool.take do |connection|
8
+ connection.execute Riddle::Query.update(
9
+ @index_name, @document_id, :sphinx_deleted => true
10
+ )
11
+ end
12
+ rescue Mysql2::Error => error
13
+ # This isn't vital, so don't raise the error.
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ class ThinkingSphinx::Deltas::IndexJob
2
+ def initialize(index_name)
3
+ @index_name = index_name
4
+ end
5
+
6
+ def perform
7
+ configuration.controller.index @index_name,
8
+ :verbose => !configuration.settings['quiet_deltas']
9
+ end
10
+
11
+ private
12
+
13
+ def configuration
14
+ @configuration ||= ThinkingSphinx::Configuration.instance
15
+ end
16
+ end
@@ -24,3 +24,6 @@ end
24
24
 
25
25
  class ThinkingSphinx::ParseError < ThinkingSphinx::QueryError
26
26
  end
27
+
28
+ class ThinkingSphinx::MixedScopesError < StandardError
29
+ end
@@ -13,8 +13,10 @@ class ThinkingSphinx::Excerpter
13
13
  end
14
14
 
15
15
  def excerpt!(text)
16
- connection.query(Riddle::Query.snippets(text, index, words, options)).
17
- first['snippet']
16
+ result = connection.query(statement_for(text)).first['snippet']
17
+
18
+ result.encode!("ISO-8859-1")
19
+ result.force_encoding("UTF-8")
18
20
  end
19
21
 
20
22
  private
@@ -22,4 +24,8 @@ class ThinkingSphinx::Excerpter
22
24
  def connection
23
25
  @connection ||= ThinkingSphinx::Connection.new
24
26
  end
27
+
28
+ def statement_for(text)
29
+ Riddle::Query.snippets(text, index, words, options)
30
+ end
25
31
  end
@@ -7,13 +7,8 @@ class ThinkingSphinx::Masks::PaginationMask
7
7
  public_methods(false).include?(method)
8
8
  end
9
9
 
10
- def current_page
11
- search.options[:page] = 1 if search.options[:page].blank?
12
- search.options[:page].to_i
13
- end
14
-
15
10
  def first_page?
16
- current_page == 1
11
+ search.current_page == 1
17
12
  end
18
13
 
19
14
  def last_page?
@@ -21,7 +16,7 @@ class ThinkingSphinx::Masks::PaginationMask
21
16
  end
22
17
 
23
18
  def next_page
24
- current_page >= total_pages ? nil : current_page + 1
19
+ search.current_page >= total_pages ? nil : search.current_page + 1
25
20
  end
26
21
 
27
22
  def next_page?
@@ -39,7 +34,7 @@ class ThinkingSphinx::Masks::PaginationMask
39
34
  end
40
35
 
41
36
  def previous_page
42
- current_page == 1 ? nil : current_page - 1
37
+ search.current_page == 1 ? nil : search.current_page - 1
43
38
  end
44
39
 
45
40
  def total_entries
@@ -11,12 +11,14 @@ require 'thinking_sphinx/middlewares/inquirer'
11
11
  require 'thinking_sphinx/middlewares/sphinxql'
12
12
  require 'thinking_sphinx/middlewares/stale_id_checker'
13
13
  require 'thinking_sphinx/middlewares/stale_id_filter'
14
+ require 'thinking_sphinx/middlewares/utf8'
14
15
 
15
16
  ThinkingSphinx::Middlewares::DEFAULT = ::Middleware::Builder.new do
16
17
  use ThinkingSphinx::Middlewares::StaleIdFilter
17
18
  use ThinkingSphinx::Middlewares::SphinxQL
18
19
  use ThinkingSphinx::Middlewares::Geographer
19
20
  use ThinkingSphinx::Middlewares::Inquirer
21
+ use ThinkingSphinx::Middlewares::UTF8
20
22
  use ThinkingSphinx::Middlewares::ActiveRecordTranslator
21
23
  use ThinkingSphinx::Middlewares::StaleIdChecker
22
24
  use ThinkingSphinx::Middlewares::Glazier
@@ -26,6 +28,7 @@ ThinkingSphinx::Middlewares::RAW_ONLY = ::Middleware::Builder.new do
26
28
  use ThinkingSphinx::Middlewares::SphinxQL
27
29
  use ThinkingSphinx::Middlewares::Geographer
28
30
  use ThinkingSphinx::Middlewares::Inquirer
31
+ use ThinkingSphinx::Middlewares::UTF8
29
32
  end
30
33
 
31
34
  ThinkingSphinx::Middlewares::IDS_ONLY = ::Middleware::Builder.new do
@@ -23,10 +23,6 @@ class ThinkingSphinx::Middlewares::SphinxQL <
23
23
  def call
24
24
  context[:indices] = indices
25
25
  context[:sphinxql] = statement
26
-
27
- if group_attribute.present?
28
- context.search.masks << ThinkingSphinx::Masks::GroupEnumeratorsMask
29
- end
30
26
  end
31
27
 
32
28
  private
@@ -0,0 +1,23 @@
1
+ class ThinkingSphinx::Middlewares::UTF8 <
2
+ ThinkingSphinx::Middlewares::Middleware
3
+
4
+ def call(contexts)
5
+ contexts.each do |context|
6
+ context[:results].each { |row| update_row row }
7
+ update_row context[:meta]
8
+ end
9
+
10
+ app.call contexts
11
+ end
12
+
13
+ private
14
+
15
+ def update_row(row)
16
+ row.each do |key, value|
17
+ next unless value.is_a?(String)
18
+
19
+ value.encode!("ISO-8859-1")
20
+ row[key] = value.force_encoding("UTF-8")
21
+ end
22
+ end
23
+ end
@@ -30,6 +30,7 @@ class ThinkingSphinx::RakeInterface
30
30
  def index(reconfigure = true)
31
31
  configure if reconfigure
32
32
  FileUtils.mkdir_p configuration.indices_location
33
+ ThinkingSphinx.before_index_hooks.each { |hook| hook.call }
33
34
  controller.index :verbose => true
34
35
  end
35
36
 
@@ -13,6 +13,14 @@ class ThinkingSphinx::RealTime::Index < Riddle::Configuration::RealtimeIndex
13
13
  super reference, options
14
14
  end
15
15
 
16
+ def add_attribute(attribute)
17
+ @attributes << attribute
18
+ end
19
+
20
+ def add_field(field)
21
+ @fields << field
22
+ end
23
+
16
24
  def attributes
17
25
  interpret_definition!
18
26
 
@@ -16,14 +16,14 @@ class ThinkingSphinx::RealTime::Index::Template
16
16
  private
17
17
 
18
18
  def add_attribute(column, name, type, options = {})
19
- index.attributes << ThinkingSphinx::RealTime::Attribute.new(
19
+ index.add_attribute ThinkingSphinx::RealTime::Attribute.new(
20
20
  ThinkingSphinx::ActiveRecord::Column.new(*column),
21
21
  options.merge(:as => name, :type => type)
22
22
  )
23
23
  end
24
24
 
25
25
  def add_field(column, name)
26
- index.fields << ThinkingSphinx::RealTime::Field.new(
26
+ index.add_field ThinkingSphinx::RealTime::Field.new(
27
27
  ThinkingSphinx::ActiveRecord::Column.new(*column), :as => name
28
28
  )
29
29
  end
@@ -8,7 +8,8 @@ class ThinkingSphinx::Search < Array
8
8
  send class )
9
9
  DEFAULT_MASKS = [
10
10
  ThinkingSphinx::Masks::PaginationMask,
11
- ThinkingSphinx::Masks::ScopesMask
11
+ ThinkingSphinx::Masks::ScopesMask,
12
+ ThinkingSphinx::Masks::GroupEnumeratorsMask
12
13
  ]
13
14
 
14
15
  instance_methods.select { |method|
@@ -23,7 +24,7 @@ class ThinkingSphinx::Search < Array
23
24
  def initialize(query = nil, options = {})
24
25
  query, options = nil, query if query.is_a?(Hash)
25
26
  @query, @options = query, options
26
- @masks = @options.delete(:masks) || DEFAULT_MASKS
27
+ @masks = @options.delete(:masks) || DEFAULT_MASKS.clone
27
28
  @middleware = @options.delete(:middleware)
28
29
 
29
30
  populate if options[:populate]
@@ -34,6 +35,11 @@ class ThinkingSphinx::Search < Array
34
35
  ThinkingSphinx::Configuration.instance
35
36
  end
36
37
 
38
+ def current_page
39
+ options[:page] = 1 if options[:page].blank?
40
+ options[:page].to_i
41
+ end
42
+
37
43
  def meta
38
44
  populate
39
45
  context[:meta]
@@ -6,7 +6,7 @@ class ThinkingSphinx::Search::Context
6
6
  @configuration = configuration || ThinkingSphinx::Configuration.instance
7
7
  @memory = {
8
8
  :results => [],
9
- :panes => ThinkingSphinx::Configuration::Defaults::PANES
9
+ :panes => ThinkingSphinx::Configuration::Defaults::PANES.clone
10
10
  }
11
11
  end
12
12
 
@@ -1,4 +1,6 @@
1
1
  class ThinkingSphinx::Search::Merger
2
+ attr_reader :search
3
+
2
4
  def initialize(search)
3
5
  @search = search
4
6
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'acceptance/spec_helper'
2
4
 
3
5
  describe 'Accessing excerpts for methods on a search result', :live => true do
@@ -11,4 +13,15 @@ describe 'Accessing excerpts for methods on a search result', :live => true do
11
13
  search.first.excerpts.title.
12
14
  should == 'American <span class="match">Gods</span>'
13
15
  end
16
+
17
+ it "handles UTF-8 text for excerpts" do
18
+ Book.create! :title => 'Война и миръ', :year => 1869
19
+ index
20
+
21
+ search = Book.search 'миръ'
22
+ search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane
23
+
24
+ search.first.excerpts.title.
25
+ should == 'Война и <span class="match">миръ</span>'
26
+ end
14
27
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'acceptance/spec_helper'
2
4
 
3
5
  describe 'Faceted searching', :live => true do
@@ -35,10 +37,11 @@ describe 'Faceted searching', :live => true do
35
37
  Book.create! :title => 'American Gods', :author => 'Neil Gaiman'
36
38
  Book.create! :title => 'Anansi Boys', :author => 'Neil Gaiman'
37
39
  Book.create! :title => 'Snuff', :author => 'Terry Pratchett'
40
+ Book.create! :title => '1Q84', :author => '村上 春樹'
38
41
  index
39
42
 
40
43
  Book.facets.to_hash[:author].should == {
41
- 'Neil Gaiman' => 2, 'Terry Pratchett' => 1
44
+ 'Neil Gaiman' => 2, 'Terry Pratchett' => 1, '村上 春樹' => 1
42
45
  }
43
46
  end
44
47
 
@@ -109,4 +109,44 @@ describe 'Index options' do
109
109
  index.sources.first.sql_query_pre.should == ["DO STUFF"]
110
110
  end
111
111
  end
112
+
113
+ context 'respecting index options over core configuration' do
114
+ before :each do
115
+ ThinkingSphinx::Configuration.instance.settings['min_infix_len'] = 2
116
+ ThinkingSphinx::Configuration.instance.settings['sql_range_step'] = 2
117
+
118
+ index.definition_block = Proc.new {
119
+ indexes title
120
+
121
+ set_property :min_infix_len => 1
122
+ set_property :sql_range_step => 20
123
+ }
124
+ index.render
125
+ end
126
+
127
+ after :each do
128
+ ThinkingSphinx::Configuration.instance.settings.delete 'min_infix_len'
129
+ ThinkingSphinx::Configuration.instance.settings.delete 'sql_range_step'
130
+ end
131
+
132
+ it "prioritises index-level options over YAML options" do
133
+ index.min_infix_len.should == 1
134
+ end
135
+
136
+ it "prioritises index-level source options" do
137
+ index.sources.first.sql_range_step.should == 20
138
+ end
139
+
140
+ it "keeps index-level options prioritised when rendered again" do
141
+ index.render
142
+
143
+ index.min_infix_len.should == 1
144
+ end
145
+
146
+ it "keeps index-level options prioritised when rendered again" do
147
+ index.render
148
+
149
+ index.sources.first.sql_range_step.should == 20
150
+ end
151
+ end
112
152
  end
@@ -47,6 +47,24 @@ describe 'Searching within a model', :live => true do
47
47
 
48
48
  Admin::Person.search('Bond').to_a.should == [person]
49
49
  end
50
+
51
+ it "raises an error if searching through an ActiveRecord scope" do
52
+ lambda {
53
+ City.ordered.search
54
+ }.should raise_error(ThinkingSphinx::MixedScopesError)
55
+ end
56
+
57
+ it "does not raise an error when searching with a default ActiveRecord scope" do
58
+ lambda {
59
+ User.search
60
+ }.should_not raise_error(ThinkingSphinx::MixedScopesError)
61
+ end
62
+
63
+ it "raises an error when searching with default and applied AR scopes" do
64
+ lambda {
65
+ User.recent.search
66
+ }.should raise_error(ThinkingSphinx::MixedScopesError)
67
+ end
50
68
  end
51
69
 
52
70
  describe 'Searching within a model with a realtime index', :live => true do
@@ -1,2 +1,3 @@
1
1
  class City < ActiveRecord::Base
2
+ scope :ordered, order(:name)
2
3
  end
@@ -1,3 +1,6 @@
1
1
  class User < ActiveRecord::Base
2
2
  has_many :articles
3
- end
3
+
4
+ default_scope { order(:id) }
5
+ scope :recent, lambda { where('created_at > ?', 1.week.ago) }
6
+ end
@@ -14,6 +14,12 @@ describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter do
14
14
  adapter.boolean_value(false).should == 0
15
15
  end
16
16
 
17
+ describe '#cast_to_string' do
18
+ it "casts the clause to characters" do
19
+ adapter.cast_to_string('foo').should == "CAST(foo AS char)"
20
+ end
21
+ end
22
+
17
23
  describe '#cast_to_timestamp' do
18
24
  it "converts to unix timestamps" do
19
25
  adapter.cast_to_timestamp('created_at').
@@ -16,11 +16,24 @@ describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter do
16
16
  end
17
17
  end
18
18
 
19
+ describe '#cast_to_string' do
20
+ it "casts the clause to characters" do
21
+ adapter.cast_to_string('foo').should == 'foo::varchar'
22
+ end
23
+ end
24
+
19
25
  describe '#cast_to_timestamp' do
20
- it "converts to unix timestamps" do
26
+ it "converts to int unix timestamps" do
21
27
  adapter.cast_to_timestamp('created_at').
22
28
  should == 'extract(epoch from created_at)::int'
23
29
  end
30
+
31
+ it "converts to bigint unix timestamps" do
32
+ ThinkingSphinx::Configuration.instance.settings['64bit_timestamps'] = true
33
+
34
+ adapter.cast_to_timestamp('created_at').
35
+ should == 'extract(epoch from created_at)::bigint'
36
+ end
24
37
  end
25
38
 
26
39
  describe '#concatenate' do
@@ -174,12 +174,27 @@ describe ThinkingSphinx::ActiveRecord::PropertySQLPresenter do
174
174
  adapter.stub :concatenate do |clause, separator|
175
175
  "CONCAT_WS('#{separator}', #{clause})"
176
176
  end
177
+ adapter.stub :cast_to_string do |clause|
178
+ "CAST(#{clause} AS varchar)"
179
+ end
177
180
 
178
181
  attribute.stub!(:columns => [column, double('column',
179
182
  :string? => false, :__stack => [], :__name => 'updated_at')])
180
183
 
181
- presenter.to_select.
182
- should == "CONCAT_WS(' ', articles.created_at) AS created_at"
184
+ presenter.to_select.should == "CONCAT_WS(',', CAST(articles.created_at AS varchar)) AS created_at"
185
+ end
186
+
187
+ it "casts and concatenates multiple columns for attributes" do
188
+ adapter.stub :concatenate do |clause, separator|
189
+ "CONCAT_WS('#{separator}', #{clause})"
190
+ end
191
+ adapter.stub :cast_to_string do |clause|
192
+ "CAST(#{clause} AS varchar)"
193
+ end
194
+
195
+ attribute.stub!(:columns => [column, column])
196
+
197
+ presenter.to_select.should == "CONCAT_WS(',', CAST(articles.created_at AS varchar), CAST(articles.created_at AS varchar)) AS created_at"
183
198
  end
184
199
  end
185
200
  end
@@ -82,6 +82,9 @@ describe ThinkingSphinx::Configuration do
82
82
 
83
83
  describe '#indices_for_references' do
84
84
  it "selects from the full index set those with matching references" do
85
+ config.preload_indices
86
+ config.indices.clear
87
+
85
88
  config.indices << double('index', :reference => :article)
86
89
  config.indices << double('index', :reference => :book)
87
90
  config.indices << double('index', :reference => :page)
@@ -23,10 +23,12 @@ describe ThinkingSphinx::Deltas::DefaultDelta do
23
23
  let(:index) { double('index', :name => 'foo_core',
24
24
  :document_id_for_key => 14) }
25
25
  let(:instance) { double('instance', :id => 7) }
26
+ let(:pool) { double }
26
27
 
27
28
  before :each do
28
- ThinkingSphinx::Connection.stub :new => connection
29
+ ThinkingSphinx::Connection.stub :pool => pool
29
30
  Riddle::Query.stub :update => 'UPDATE STATEMENT'
31
+ pool.stub(:take).and_yield(connection)
30
32
  end
31
33
 
32
34
  it "updates the deleted flag to false" do
@@ -7,7 +7,7 @@ require 'thinking_sphinx/masks/pagination_mask'
7
7
 
8
8
  describe ThinkingSphinx::Masks::PaginationMask do
9
9
  let(:search) { double('search', :options => {}, :meta => {},
10
- :per_page => 20) }
10
+ :per_page => 20, :current_page => 1) }
11
11
  let(:mask) { ThinkingSphinx::Masks::PaginationMask.new search }
12
12
 
13
13
  describe '#first_page?' do
@@ -16,7 +16,7 @@ describe ThinkingSphinx::Masks::PaginationMask do
16
16
  end
17
17
 
18
18
  it "returns false on other pages" do
19
- search.options[:page] = 2
19
+ search.stub :current_page => 2
20
20
 
21
21
  mask.should_not be_first_page
22
22
  end
@@ -28,7 +28,7 @@ describe ThinkingSphinx::Masks::PaginationMask do
28
28
  end
29
29
 
30
30
  it "is true when there's no more pages" do
31
- search.options[:page] = 3
31
+ search.stub :current_page => 3
32
32
 
33
33
  mask.should be_last_page
34
34
  end
@@ -48,7 +48,7 @@ describe ThinkingSphinx::Masks::PaginationMask do
48
48
  end
49
49
 
50
50
  it "should return nil if on the last page" do
51
- search.options[:page] = 3
51
+ search.stub :current_page => 3
52
52
 
53
53
  mask.next_page.should be_nil
54
54
  end
@@ -64,7 +64,7 @@ describe ThinkingSphinx::Masks::PaginationMask do
64
64
  end
65
65
 
66
66
  it "is false when there's no more pages" do
67
- search.options[:page] = 3
67
+ search.stub :current_page => 3
68
68
 
69
69
  mask.next_page?.should be_false
70
70
  end
@@ -76,7 +76,7 @@ describe ThinkingSphinx::Masks::PaginationMask do
76
76
  end
77
77
 
78
78
  it "should return one less than the current page" do
79
- search.options[:page] = 2
79
+ search.stub :current_page => 2
80
80
 
81
81
  mask.previous_page.should == 1
82
82
  end
@@ -26,7 +26,6 @@ describe ThinkingSphinx::Middlewares::SphinxQL do
26
26
  before :each do
27
27
  stub_const 'Riddle::Query::Select', double(:new => sphinx_sql)
28
28
  stub_const 'ThinkingSphinx::Search::Query', double(:new => query)
29
- stub_const 'ThinkingSphinx::Masks::GroupEnumeratorsMask', double
30
29
  stub_const 'ThinkingSphinx::IndexSet', double(:new => index_set)
31
30
 
32
31
  context.stub :search => search, :configuration => configuration
@@ -228,16 +227,6 @@ describe ThinkingSphinx::Middlewares::SphinxQL do
228
227
  middleware.call [context]
229
228
  end
230
229
 
231
- it "adds the group enumerator mask when using :group_by" do
232
- search.options[:group_by] = :foreign_id
233
- search.stub :masks => []
234
- sphinx_sql.stub :group_by => sphinx_sql, :values => sphinx_sql
235
-
236
- middleware.call [context]
237
-
238
- search.masks.should include(ThinkingSphinx::Masks::GroupEnumeratorsMask)
239
- end
240
-
241
230
  it "appends a sort within group clause to the query" do
242
231
  search.options[:order_group_by] = :title
243
232
 
@@ -37,6 +37,7 @@ describe ThinkingSphinx::RakeInterface do
37
37
  let(:controller) { double('controller', :index => true) }
38
38
 
39
39
  before :each do
40
+ ThinkingSphinx.stub :before_index_hooks => []
40
41
  configuration.stub(
41
42
  :configuration_file => '/path/to/foo.conf',
42
43
  :render_to_file => true,
@@ -64,6 +65,15 @@ describe ThinkingSphinx::RakeInterface do
64
65
  interface.index
65
66
  end
66
67
 
68
+ it "calls all registered hooks" do
69
+ called = false
70
+ ThinkingSphinx.before_index_hooks << Proc.new { called = true }
71
+
72
+ interface.index
73
+
74
+ called.should be_true
75
+ end
76
+
67
77
  it "indexes all indices verbosely" do
68
78
  controller.should_receive(:index).with(:verbose => true)
69
79
 
@@ -3,7 +3,7 @@ $:.push File.expand_path('../lib', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = 'thinking-sphinx'
6
- s.version = '3.0.2'
6
+ s.version = '3.0.3'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.authors = ["Pat Allan"]
9
9
  s.email = ["pat@freelancing-gods.com"]
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
24
24
  s.add_runtime_dependency 'builder', '>= 2.1.2'
25
25
  s.add_runtime_dependency 'middleware', '>= 0.1.0'
26
26
  s.add_runtime_dependency 'innertube', '>= 1.0.2'
27
- s.add_runtime_dependency 'riddle', '>= 1.5.4'
27
+ s.add_runtime_dependency 'riddle', '>= 1.5.6'
28
28
 
29
29
  s.add_development_dependency 'appraisal', '~> 0.4.0'
30
30
  s.add_development_dependency 'combustion', '~> 0.4.0'
metadata CHANGED
@@ -1,88 +1,100 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thinking-sphinx
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.2
4
+ version: 3.0.3
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Pat Allan
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2013-03-23 00:00:00.000000000 Z
12
+ date: 2013-05-08 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: activerecord
15
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
16
18
  requirements:
17
- - - '>='
19
+ - - ! '>='
18
20
  - !ruby/object:Gem::Version
19
21
  version: 3.1.0
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
23
26
  requirements:
24
- - - '>='
27
+ - - ! '>='
25
28
  - !ruby/object:Gem::Version
26
29
  version: 3.1.0
27
30
  - !ruby/object:Gem::Dependency
28
31
  name: builder
29
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
30
34
  requirements:
31
- - - '>='
35
+ - - ! '>='
32
36
  - !ruby/object:Gem::Version
33
37
  version: 2.1.2
34
38
  type: :runtime
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
37
42
  requirements:
38
- - - '>='
43
+ - - ! '>='
39
44
  - !ruby/object:Gem::Version
40
45
  version: 2.1.2
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: middleware
43
48
  requirement: !ruby/object:Gem::Requirement
49
+ none: false
44
50
  requirements:
45
- - - '>='
51
+ - - ! '>='
46
52
  - !ruby/object:Gem::Version
47
53
  version: 0.1.0
48
54
  type: :runtime
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
51
58
  requirements:
52
- - - '>='
59
+ - - ! '>='
53
60
  - !ruby/object:Gem::Version
54
61
  version: 0.1.0
55
62
  - !ruby/object:Gem::Dependency
56
63
  name: innertube
57
64
  requirement: !ruby/object:Gem::Requirement
65
+ none: false
58
66
  requirements:
59
- - - '>='
67
+ - - ! '>='
60
68
  - !ruby/object:Gem::Version
61
69
  version: 1.0.2
62
70
  type: :runtime
63
71
  prerelease: false
64
72
  version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
65
74
  requirements:
66
- - - '>='
75
+ - - ! '>='
67
76
  - !ruby/object:Gem::Version
68
77
  version: 1.0.2
69
78
  - !ruby/object:Gem::Dependency
70
79
  name: riddle
71
80
  requirement: !ruby/object:Gem::Requirement
81
+ none: false
72
82
  requirements:
73
- - - '>='
83
+ - - ! '>='
74
84
  - !ruby/object:Gem::Version
75
- version: 1.5.4
85
+ version: 1.5.6
76
86
  type: :runtime
77
87
  prerelease: false
78
88
  version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
79
90
  requirements:
80
- - - '>='
91
+ - - ! '>='
81
92
  - !ruby/object:Gem::Version
82
- version: 1.5.4
93
+ version: 1.5.6
83
94
  - !ruby/object:Gem::Dependency
84
95
  name: appraisal
85
96
  requirement: !ruby/object:Gem::Requirement
97
+ none: false
86
98
  requirements:
87
99
  - - ~>
88
100
  - !ruby/object:Gem::Version
@@ -90,6 +102,7 @@ dependencies:
90
102
  type: :development
91
103
  prerelease: false
92
104
  version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
93
106
  requirements:
94
107
  - - ~>
95
108
  - !ruby/object:Gem::Version
@@ -97,6 +110,7 @@ dependencies:
97
110
  - !ruby/object:Gem::Dependency
98
111
  name: combustion
99
112
  requirement: !ruby/object:Gem::Requirement
113
+ none: false
100
114
  requirements:
101
115
  - - ~>
102
116
  - !ruby/object:Gem::Version
@@ -104,6 +118,7 @@ dependencies:
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
107
122
  requirements:
108
123
  - - ~>
109
124
  - !ruby/object:Gem::Version
@@ -111,6 +126,7 @@ dependencies:
111
126
  - !ruby/object:Gem::Dependency
112
127
  name: database_cleaner
113
128
  requirement: !ruby/object:Gem::Requirement
129
+ none: false
114
130
  requirements:
115
131
  - - ~>
116
132
  - !ruby/object:Gem::Version
@@ -118,6 +134,7 @@ dependencies:
118
134
  type: :development
119
135
  prerelease: false
120
136
  version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
121
138
  requirements:
122
139
  - - ~>
123
140
  - !ruby/object:Gem::Version
@@ -125,6 +142,7 @@ dependencies:
125
142
  - !ruby/object:Gem::Dependency
126
143
  name: rspec
127
144
  requirement: !ruby/object:Gem::Requirement
145
+ none: false
128
146
  requirements:
129
147
  - - ~>
130
148
  - !ruby/object:Gem::Version
@@ -132,6 +150,7 @@ dependencies:
132
150
  type: :development
133
151
  prerelease: false
134
152
  version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
135
154
  requirements:
136
155
  - - ~>
137
156
  - !ruby/object:Gem::Version
@@ -202,6 +221,8 @@ files:
202
221
  - lib/thinking_sphinx/core/property.rb
203
222
  - lib/thinking_sphinx/deltas.rb
204
223
  - lib/thinking_sphinx/deltas/default_delta.rb
224
+ - lib/thinking_sphinx/deltas/delete_job.rb
225
+ - lib/thinking_sphinx/deltas/index_job.rb
205
226
  - lib/thinking_sphinx/errors.rb
206
227
  - lib/thinking_sphinx/excerpter.rb
207
228
  - lib/thinking_sphinx/facet.rb
@@ -226,6 +247,7 @@ files:
226
247
  - lib/thinking_sphinx/middlewares/sphinxql.rb
227
248
  - lib/thinking_sphinx/middlewares/stale_id_checker.rb
228
249
  - lib/thinking_sphinx/middlewares/stale_id_filter.rb
250
+ - lib/thinking_sphinx/middlewares/utf8.rb
229
251
  - lib/thinking_sphinx/panes.rb
230
252
  - lib/thinking_sphinx/panes/attributes_pane.rb
231
253
  - lib/thinking_sphinx/panes/distance_pane.rb
@@ -371,26 +393,33 @@ files:
371
393
  - thinking-sphinx.gemspec
372
394
  homepage: http://pat.github.com/ts/en
373
395
  licenses: []
374
- metadata: {}
375
396
  post_install_message:
376
397
  rdoc_options: []
377
398
  require_paths:
378
399
  - lib
379
400
  required_ruby_version: !ruby/object:Gem::Requirement
401
+ none: false
380
402
  requirements:
381
- - - '>='
403
+ - - ! '>='
382
404
  - !ruby/object:Gem::Version
383
405
  version: '0'
406
+ segments:
407
+ - 0
408
+ hash: 4005802565131001666
384
409
  required_rubygems_version: !ruby/object:Gem::Requirement
410
+ none: false
385
411
  requirements:
386
- - - '>='
412
+ - - ! '>='
387
413
  - !ruby/object:Gem::Version
388
414
  version: '0'
415
+ segments:
416
+ - 0
417
+ hash: 4005802565131001666
389
418
  requirements: []
390
419
  rubyforge_project: thinking-sphinx
391
- rubygems_version: 2.0.0
420
+ rubygems_version: 1.8.23
392
421
  signing_key:
393
- specification_version: 4
422
+ specification_version: 3
394
423
  summary: A smart wrapper over Sphinx for ActiveRecord
395
424
  test_files:
396
425
  - spec/acceptance/association_scoping_spec.rb
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: 2825535169716d44b3c2a897d70bdd6c44638b17
4
- data.tar.gz: 30d74a154abf22f00795be4eaae8cb69e16fff19
5
- SHA512:
6
- metadata.gz: 4f33cf2627e2944ecee27c689a60770a201a4eb54de9fd97354b3eec27cd07d0d847d79a003b4df8e188f5dc3ecdf2a36ac7003cb8cb63f158f4486f3f75f1cc
7
- data.tar.gz: a66b7764fcb538650f61931d4a5bddaf9d708098db223606a120f503a1eecbbe533e6b048b942a10fe157b8a11b593b0ff4e27a80e7934378597cfe2f6fd284a