thinking-sphinx 3.0.6 → 3.1.0

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -6
  3. data/Appraisals +5 -5
  4. data/Gemfile +3 -3
  5. data/HISTORY +32 -0
  6. data/README.textile +16 -5
  7. data/gemfiles/rails_3_2.gemfile +2 -1
  8. data/gemfiles/rails_4_0.gemfile +3 -2
  9. data/gemfiles/{rails_3_1.gemfile → rails_4_1.gemfile} +3 -2
  10. data/lib/thinking_sphinx.rb +5 -0
  11. data/lib/thinking_sphinx/active_record.rb +2 -1
  12. data/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +1 -1
  13. data/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +6 -7
  14. data/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +2 -2
  15. data/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +1 -1
  16. data/lib/thinking_sphinx/active_record/column_sql_presenter.rb +34 -0
  17. data/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +4 -0
  18. data/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb +4 -0
  19. data/lib/thinking_sphinx/active_record/index.rb +2 -1
  20. data/lib/thinking_sphinx/active_record/interpreter.rb +7 -0
  21. data/lib/thinking_sphinx/active_record/property.rb +1 -1
  22. data/lib/thinking_sphinx/active_record/property_sql_presenter.rb +10 -16
  23. data/lib/thinking_sphinx/active_record/sql_builder.rb +7 -1
  24. data/lib/thinking_sphinx/active_record/sql_builder/query.rb +0 -7
  25. data/lib/thinking_sphinx/active_record/sql_source.rb +20 -20
  26. data/lib/thinking_sphinx/active_record/sql_source/template.rb +1 -1
  27. data/lib/thinking_sphinx/capistrano.rb +6 -65
  28. data/lib/thinking_sphinx/capistrano/v2.rb +58 -0
  29. data/lib/thinking_sphinx/capistrano/v3.rb +101 -0
  30. data/lib/thinking_sphinx/configuration.rb +8 -3
  31. data/lib/thinking_sphinx/configuration/distributed_indices.rb +29 -0
  32. data/lib/thinking_sphinx/connection.rb +90 -34
  33. data/lib/thinking_sphinx/controller.rb +20 -0
  34. data/lib/thinking_sphinx/core/index.rb +4 -0
  35. data/lib/thinking_sphinx/deletion.rb +15 -11
  36. data/lib/thinking_sphinx/deltas.rb +9 -0
  37. data/lib/thinking_sphinx/deltas/default_delta.rb +2 -0
  38. data/lib/thinking_sphinx/distributed.rb +5 -0
  39. data/lib/thinking_sphinx/distributed/index.rb +24 -0
  40. data/lib/thinking_sphinx/excerpter.rb +7 -3
  41. data/lib/thinking_sphinx/facet_search.rb +1 -1
  42. data/lib/thinking_sphinx/index.rb +2 -6
  43. data/lib/thinking_sphinx/index_set.rb +10 -8
  44. data/lib/thinking_sphinx/middlewares.rb +0 -2
  45. data/lib/thinking_sphinx/middlewares/active_record_translator.rb +1 -0
  46. data/lib/thinking_sphinx/middlewares/geographer.rb +1 -1
  47. data/lib/thinking_sphinx/middlewares/sphinxql.rb +8 -6
  48. data/lib/thinking_sphinx/middlewares/utf8.rb +6 -1
  49. data/lib/thinking_sphinx/query.rb +9 -0
  50. data/lib/thinking_sphinx/railtie.rb +0 -13
  51. data/lib/thinking_sphinx/search/query.rb +3 -21
  52. data/lib/thinking_sphinx/sphinxql.rb +1 -1
  53. data/lib/thinking_sphinx/wildcard.rb +34 -0
  54. data/spec/acceptance/geosearching_spec.rb +13 -0
  55. data/spec/acceptance/indexing_spec.rb +27 -0
  56. data/spec/acceptance/remove_deleted_records_spec.rb +8 -0
  57. data/spec/acceptance/searching_with_sti_spec.rb +7 -0
  58. data/spec/acceptance/searching_within_a_model_spec.rb +8 -0
  59. data/spec/acceptance/sorting_search_results_spec.rb +1 -1
  60. data/spec/acceptance/spec_helper.rb +13 -0
  61. data/spec/acceptance/specifying_sql_spec.rb +2 -2
  62. data/spec/acceptance/support/sphinx_controller.rb +5 -5
  63. data/spec/acceptance/support/sphinx_helpers.rb +3 -0
  64. data/spec/acceptance/suspended_deltas_spec.rb +34 -0
  65. data/spec/internal/config/database.yml +1 -0
  66. data/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb +13 -6
  67. data/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +2 -2
  68. data/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb +7 -0
  69. data/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb +6 -0
  70. data/spec/thinking_sphinx/active_record/interpreter_spec.rb +27 -0
  71. data/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb +18 -7
  72. data/spec/thinking_sphinx/active_record/sql_builder_spec.rb +17 -7
  73. data/spec/thinking_sphinx/active_record/sql_source_spec.rb +84 -82
  74. data/spec/thinking_sphinx/configuration_spec.rb +5 -4
  75. data/spec/thinking_sphinx/connection_spec.rb +1 -1
  76. data/spec/thinking_sphinx/deletion_spec.rb +10 -28
  77. data/spec/thinking_sphinx/excerpter_spec.rb +3 -3
  78. data/spec/thinking_sphinx/facet_search_spec.rb +13 -4
  79. data/spec/thinking_sphinx/index_set_spec.rb +9 -4
  80. data/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb +8 -0
  81. data/spec/thinking_sphinx/middlewares/geographer_spec.rb +7 -7
  82. data/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +17 -1
  83. data/spec/thinking_sphinx/search/query_spec.rb +10 -53
  84. data/spec/thinking_sphinx/wildcard_spec.rb +41 -0
  85. data/thinking-sphinx.gemspec +6 -5
  86. metadata +38 -14
  87. data/lib/thinking_sphinx/active_record/associations.rb +0 -98
  88. data/spec/thinking_sphinx/active_record/associations_spec.rb +0 -230
@@ -0,0 +1,20 @@
1
+ class ThinkingSphinx::Controller < Riddle::Controller
2
+ def index(*indices)
3
+ options = indices.extract_options!
4
+ indices << '--all' if indices.empty?
5
+
6
+ indices = indices.reject { |index| File.exists? guard_file(index) }
7
+ return if indices.empty?
8
+
9
+ indices.each { |index| FileUtils.touch guard_file(index) }
10
+ super(*(indices + [options]))
11
+ indices.each { |index| FileUtils.rm guard_file(index) }
12
+ end
13
+
14
+ def guard_file(index)
15
+ File.join(
16
+ ThinkingSphinx::Configuration.instance.indices_location,
17
+ "ts-#{index}.tmp"
18
+ )
19
+ end
20
+ end
@@ -22,6 +22,10 @@ module ThinkingSphinx::Core::Index
22
22
  false
23
23
  end
24
24
 
25
+ def distributed?
26
+ false
27
+ end
28
+
25
29
  def document_id_for_key(key)
26
30
  key * config.indices.count + offset
27
31
  end
@@ -1,25 +1,27 @@
1
1
  class ThinkingSphinx::Deletion
2
2
  delegate :name, :to => :index
3
3
 
4
- def self.perform(index, instance)
4
+ def self.perform(index, ids)
5
+ return if index.distributed?
6
+
5
7
  {
6
8
  'plain' => PlainDeletion,
7
9
  'rt' => RealtimeDeletion
8
- }[index.type].new(index, instance).perform
10
+ }[index.type].new(index, ids).perform
9
11
  rescue ThinkingSphinx::ConnectionError => error
10
12
  # This isn't vital, so don't raise the error.
11
13
  end
12
14
 
13
- def initialize(index, instance)
14
- @index, @instance = index, instance
15
+ def initialize(index, ids)
16
+ @index, @ids = index, Array(ids)
15
17
  end
16
18
 
17
19
  private
18
20
 
19
- attr_reader :index, :instance
21
+ attr_reader :index, :ids
20
22
 
21
- def document_id_for_key
22
- index.document_id_for_key instance.id
23
+ def document_ids_for_keys
24
+ ids.collect { |id| index.document_id_for_key id }
23
25
  end
24
26
 
25
27
  def execute(statement)
@@ -30,15 +32,17 @@ class ThinkingSphinx::Deletion
30
32
 
31
33
  class RealtimeDeletion < ThinkingSphinx::Deletion
32
34
  def perform
33
- execute Riddle::Query::Delete.new(name, document_id_for_key).to_sql
35
+ execute Riddle::Query::Delete.new(name, document_ids_for_keys).to_sql
34
36
  end
35
37
  end
36
38
 
37
39
  class PlainDeletion < ThinkingSphinx::Deletion
38
40
  def perform
39
- execute Riddle::Query.update(
40
- name, document_id_for_key, :sphinx_deleted => true
41
- )
41
+ execute <<-SQL
42
+ UPDATE #{name}
43
+ SET sphinx_deleted = 1
44
+ WHERE id IN (#{document_ids_for_keys.join(', ')})
45
+ SQL
42
46
  end
43
47
  end
44
48
  end
@@ -30,6 +30,15 @@ module ThinkingSphinx::Deltas
30
30
  end
31
31
  end
32
32
 
33
+ def self.suspend_and_update(reference, &block)
34
+ suspend reference, &block
35
+
36
+ ids = reference.to_s.camelize.constantize.where(delta: true).pluck(:id)
37
+ config.indices_for_references(reference).each do |index|
38
+ ThinkingSphinx::Deletion.perform index, ids unless index.delta?
39
+ end
40
+ end
41
+
33
42
  def self.suspend!
34
43
  @suspended = true
35
44
  end
@@ -6,6 +6,8 @@ class ThinkingSphinx::Deltas::DefaultDelta
6
6
  end
7
7
 
8
8
  def clause(delta_source = false)
9
+ return nil unless delta_source
10
+
9
11
  "#{adapter.quoted_table_name}.#{quoted_column} = #{adapter.boolean_value delta_source}"
10
12
  end
11
13
 
@@ -0,0 +1,5 @@
1
+ module ThinkingSphinx::Distributed
2
+ #
3
+ end
4
+
5
+ require 'thinking_sphinx/distributed/index'
@@ -0,0 +1,24 @@
1
+ class ThinkingSphinx::Distributed::Index <
2
+ Riddle::Configuration::DistributedIndex
3
+
4
+ attr_reader :reference, :options
5
+
6
+ def initialize(reference)
7
+ @reference = reference
8
+ @options = {}
9
+
10
+ super reference.to_s.gsub('/', '_')
11
+ end
12
+
13
+ def delta?
14
+ false
15
+ end
16
+
17
+ def distributed?
18
+ true
19
+ end
20
+
21
+ def model
22
+ @model ||= reference.to_s.camelize.constantize
23
+ end
24
+ end
@@ -15,11 +15,10 @@ class ThinkingSphinx::Excerpter
15
15
 
16
16
  def excerpt!(text)
17
17
  result = ThinkingSphinx::Connection.take do |connection|
18
- connection.query(statement_for(text)).first['snippet']
18
+ connection.execute(statement_for(text)).first['snippet']
19
19
  end
20
20
 
21
- ThinkingSphinx::Configuration.instance.settings['utf8'] ? result :
22
- ThinkingSphinx::UTF8.encode(result)
21
+ encoded? ? result : ThinkingSphinx::UTF8.encode(result)
23
22
  end
24
23
 
25
24
  private
@@ -27,4 +26,9 @@ class ThinkingSphinx::Excerpter
27
26
  def statement_for(text)
28
27
  Riddle::Query.snippets(text, index, words, options)
29
28
  end
29
+
30
+ def encoded?
31
+ ThinkingSphinx::Configuration.instance.settings['utf8'].nil? ||
32
+ ThinkingSphinx::Configuration.instance.settings['utf8']
33
+ end
30
34
  end
@@ -105,7 +105,7 @@ class ThinkingSphinx::FacetSearch
105
105
  ", #{ThinkingSphinx::SphinxQL.group_by}, #{ThinkingSphinx::SphinxQL.count}",
106
106
  :group_by => facet.name,
107
107
  :indices => index_names_for(facet),
108
- :max_matches => limit,
108
+ :max_matches => max_matches,
109
109
  :limit => limit
110
110
  )
111
111
  end
@@ -11,16 +11,12 @@ class ThinkingSphinx::Index
11
11
  defaults = ThinkingSphinx::Configuration.instance.
12
12
  settings['index_options'] || {}
13
13
  defaults.symbolize_keys!
14
-
14
+
15
15
  @reference, @options, @block = reference, defaults.merge(options), block
16
16
  end
17
17
 
18
18
  def indices
19
- if options[:delta]
20
- delta_indices
21
- else
22
- [single_index]
23
- end
19
+ options[:delta] ? delta_indices : [single_index]
24
20
  end
25
21
 
26
22
  private
@@ -19,8 +19,10 @@ class ThinkingSphinx::IndexSet
19
19
 
20
20
  private
21
21
 
22
+ attr_reader :classes, :configuration, :index_names
23
+
22
24
  def classes_and_ancestors
23
- @classes_and_ancestors ||= @classes.collect { |model|
25
+ @classes_and_ancestors ||= classes.collect { |model|
24
26
  model.ancestors.take_while { |klass|
25
27
  klass != ActiveRecord::Base
26
28
  }.select { |klass|
@@ -30,15 +32,15 @@ class ThinkingSphinx::IndexSet
30
32
  end
31
33
 
32
34
  def indices
33
- @configuration.preload_indices
34
-
35
- return @configuration.indices.select { |index|
36
- @index_names.include?(index.name)
37
- } if @index_names && @index_names.any?
35
+ configuration.preload_indices
38
36
 
39
- return @configuration.indices if @classes.empty?
37
+ return configuration.indices.select { |index|
38
+ index_names.include?(index.name)
39
+ } if index_names && index_names.any?
40
40
 
41
- @configuration.indices_for_references(*references)
41
+ everything = classes.empty? ? configuration.indices :
42
+ configuration.indices_for_references(*references)
43
+ everything.reject &:distributed?
42
44
  end
43
45
 
44
46
  def references
@@ -15,7 +15,6 @@ module ThinkingSphinx::Middlewares
15
15
  DEFAULT = ::Middleware::Builder.new do
16
16
  use StaleIdFilter
17
17
  ThinkingSphinx::Middlewares.use self, BASE_MIDDLEWARES
18
- use UTF8
19
18
  use ActiveRecordTranslator
20
19
  use StaleIdChecker
21
20
  use Glazier
@@ -23,7 +22,6 @@ module ThinkingSphinx::Middlewares
23
22
 
24
23
  RAW_ONLY = ::Middleware::Builder.new do
25
24
  ThinkingSphinx::Middlewares.use self, BASE_MIDDLEWARES
26
- use UTF8
27
25
  end
28
26
 
29
27
  IDS_ONLY = ::Middleware::Builder.new do
@@ -69,6 +69,7 @@ class ThinkingSphinx::Middlewares::ActiveRecordTranslator <
69
69
  relation = relation.joins sql_options[:joins] if sql_options[:joins]
70
70
  relation = relation.order sql_options[:order] if sql_options[:order]
71
71
  relation = relation.select sql_options[:select] if sql_options[:select]
72
+ relation = relation.group sql_options[:group] if sql_options[:group]
72
73
  relation
73
74
  end
74
75
 
@@ -21,7 +21,7 @@ class ThinkingSphinx::Middlewares::Geographer <
21
21
  def call
22
22
  return unless geo
23
23
 
24
- context[:sphinxql].values geodist_clause
24
+ context[:sphinxql].prepend_values geodist_clause
25
25
  context[:panes] << ThinkingSphinx::Panes::DistancePane
26
26
  end
27
27
 
@@ -1,10 +1,10 @@
1
1
  class ThinkingSphinx::Middlewares::SphinxQL <
2
2
  ThinkingSphinx::Middlewares::Middleware
3
3
 
4
- SELECT_OPTIONS = [:ranker, :max_matches, :cutoff, :max_query_time,
5
- :retry_count, :retry_delay, :field_weights, :index_weights, :reverse_scan,
6
- :comment, :agent_query_timeout, :boolean_simplify, :global_idf, :idf,
7
- :sort_method]
4
+ SELECT_OPTIONS = [:agent_query_timeout, :boolean_simplify, :comment, :cutoff,
5
+ :field_weights, :global_idf, :idf, :index_weights, :max_matches,
6
+ :max_query_time, :max_predicted_time, :ranker, :retry_count, :retry_delay,
7
+ :reverse_scan, :sort_method]
8
8
 
9
9
  def call(contexts)
10
10
  contexts.each do |context|
@@ -197,7 +197,7 @@ SQL
197
197
  end
198
198
 
199
199
  def scope_by_values
200
- query.values values if values.present?
200
+ query.values(values.present? ? values : '*')
201
201
  end
202
202
 
203
203
  def scope_by_extended_query
@@ -225,8 +225,10 @@ SQL
225
225
  end
226
226
 
227
227
  def scope_by_group
228
- query.group_by group_attribute if group_attribute.present?
228
+ query.group_by group_attribute if group_attribute.present?
229
+ query.group_best options[:group_best] if options[:group_best]
229
230
  query.order_within_group_by group_order_clause if group_order_clause.present?
231
+ query.having options[:having] if options[:having]
230
232
  end
231
233
 
232
234
  def scope_by_pagination
@@ -5,13 +5,18 @@ 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 unless ThinkingSphinx::Configuration.instance.settings['utf8']
8
+ end unless encoded?
9
9
 
10
10
  app.call contexts
11
11
  end
12
12
 
13
13
  private
14
14
 
15
+ def encoded?
16
+ ThinkingSphinx::Configuration.instance.settings['utf8'].nil? ||
17
+ ThinkingSphinx::Configuration.instance.settings['utf8']
18
+ end
19
+
15
20
  def update_row(row)
16
21
  row.each do |key, value|
17
22
  next unless value.is_a?(String)
@@ -0,0 +1,9 @@
1
+ module ThinkingSphinx::Query
2
+ def self.escape(query)
3
+ Riddle::Query.escape query
4
+ end
5
+
6
+ def self.wildcard(query, pattern = true)
7
+ ThinkingSphinx::Wildcard.call query, pattern
8
+ end
9
+ end
@@ -7,16 +7,3 @@ class ThinkingSphinx::Railtie < Rails::Railtie
7
7
  load File.expand_path('../tasks.rb', __FILE__)
8
8
  end
9
9
  end
10
-
11
- # Add 'app/indices' path to Rails Engines
12
- module ThinkingSphinx::EnginePaths
13
- extend ActiveSupport::Concern
14
-
15
- included do
16
- initializer :add_indices_path do
17
- paths.add "app/indices"
18
- end
19
- end
20
- end
21
-
22
- Rails::Engine.send :include, ThinkingSphinx::EnginePaths
@@ -1,8 +1,5 @@
1
1
  # encoding: utf-8
2
-
3
2
  class ThinkingSphinx::Search::Query
4
- DEFAULT_TOKEN = /[\p{Word}\\][\p{Word}\\@]+/
5
-
6
3
  attr_reader :keywords, :conditions, :star
7
4
 
8
5
  def initialize(keywords = '', conditions = {}, star = false)
@@ -20,24 +17,9 @@ class ThinkingSphinx::Search::Query
20
17
  private
21
18
 
22
19
  def star_keyword(keyword, key = nil)
23
- unless star && (key.nil? || key.to_s != 'sphinx_internal_class_name')
24
- return keyword.to_s
25
- end
20
+ return keyword.to_s unless star
21
+ return keyword.to_s if key.to_s == 'sphinx_internal_class_name'
26
22
 
27
- token = star.is_a?(Regexp) ? star : DEFAULT_TOKEN
28
- keyword.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
29
- pre, proper, post = $`, $&, $'
30
- # E.g. "@foo", "/2", "~3", but not as part of a token
31
- is_operator = pre.match(%r{\A(\W|^)[@~/]\Z}) ||
32
- pre.match(%r{(\W|^)@\([^\)]*$})
33
- # E.g. "foo bar", with quotes
34
- is_quote = proper[/^".*"$/]
35
- has_star = post[/\*$/] || pre[/^\*/]
36
- if is_operator || is_quote || has_star
37
- proper
38
- else
39
- "*#{proper}*"
40
- end
41
- end
23
+ ThinkingSphinx::Query.wildcard keyword, star
42
24
  end
43
25
  end
@@ -13,5 +13,5 @@ module ThinkingSphinx::SphinxQL
13
13
  self.count = '@count'
14
14
  end
15
15
 
16
- self.variables!
16
+ self.functions!
17
17
  end
@@ -0,0 +1,34 @@
1
+ class ThinkingSphinx::Wildcard
2
+ DEFAULT_TOKEN = /\p{Word}+/
3
+
4
+ def self.call(query, pattern = DEFAULT_TOKEN)
5
+ new(query, pattern).call
6
+ end
7
+
8
+ def initialize(query, pattern = DEFAULT_TOKEN)
9
+ @query = query || ''
10
+ @pattern = pattern.is_a?(Regexp) ? pattern : DEFAULT_TOKEN
11
+ end
12
+
13
+ def call
14
+ query.gsub(/("#{pattern}(.*?#{pattern})?"|(?![!-])#{pattern})/u) do
15
+ pre, proper, post = $`, $&, $'
16
+ # E.g. "@foo", "/2", "~3", but not as part of a token pattern
17
+ is_operator = pre == '@' ||
18
+ pre.match(%r{([^\\]+|\A)[~/]\Z}) ||
19
+ pre.match(%r{(\W|^)@\([^\)]*$})
20
+ # E.g. "foo bar", with quotes
21
+ is_quote = proper[/^".*"$/]
22
+ has_star = post[/\*$/] || pre[/^\*/]
23
+ if is_operator || is_quote || has_star
24
+ proper
25
+ else
26
+ "*#{proper}*"
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :query, :pattern
34
+ end
@@ -36,4 +36,17 @@ describe 'Searching by latitude and longitude', :live => true do
36
36
  cities.first.geodist.should == 250326.906250
37
37
  end
38
38
  end
39
+
40
+ it "handles custom select clauses that refer to the distance" do
41
+ mel = City.create :name => 'Melbourne', :lat => -0.6599720, :lng => 2.530082
42
+ syd = City.create :name => 'Sydney', :lat => -0.5909679, :lng => 2.639131
43
+ bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838
44
+ index
45
+
46
+ City.search(
47
+ :geo => [-0.616241, 2.602712],
48
+ :with => {:geodist => 0.0..470_000.0},
49
+ :select => "*, geodist as custom_weight"
50
+ ).to_a.should == [mel, syd]
51
+ end
39
52
  end