thinking-sphinx 3.0.2 → 3.0.3

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 (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