thinking-sphinx 3.0.6 → 3.1.0

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