DrMark-thinking-sphinx 0.9.9 → 1.1.6

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 (77) hide show
  1. data/README +64 -2
  2. data/lib/thinking_sphinx.rb +88 -11
  3. data/lib/thinking_sphinx/active_record.rb +136 -21
  4. data/lib/thinking_sphinx/active_record/delta.rb +43 -62
  5. data/lib/thinking_sphinx/active_record/has_many_association.rb +1 -1
  6. data/lib/thinking_sphinx/active_record/search.rb +7 -0
  7. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  8. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  9. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +130 -0
  10. data/lib/thinking_sphinx/association.rb +17 -0
  11. data/lib/thinking_sphinx/attribute.rb +171 -97
  12. data/lib/thinking_sphinx/collection.rb +126 -2
  13. data/lib/thinking_sphinx/configuration.rb +120 -171
  14. data/lib/thinking_sphinx/core/string.rb +15 -0
  15. data/lib/thinking_sphinx/deltas.rb +27 -0
  16. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  17. data/lib/thinking_sphinx/deltas/default_delta.rb +67 -0
  18. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  19. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  20. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  22. data/lib/thinking_sphinx/facet.rb +58 -0
  23. data/lib/thinking_sphinx/facet_collection.rb +60 -0
  24. data/lib/thinking_sphinx/field.rb +18 -52
  25. data/lib/thinking_sphinx/index.rb +246 -199
  26. data/lib/thinking_sphinx/index/builder.rb +85 -16
  27. data/lib/thinking_sphinx/rails_additions.rb +85 -5
  28. data/lib/thinking_sphinx/search.rb +459 -190
  29. data/lib/thinking_sphinx/tasks.rb +128 -0
  30. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +53 -124
  31. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +2 -2
  32. data/spec/unit/thinking_sphinx/active_record_spec.rb +110 -30
  33. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -149
  34. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  35. data/spec/unit/thinking_sphinx/configuration_spec.rb +54 -412
  36. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  37. data/spec/unit/thinking_sphinx/field_spec.rb +0 -79
  38. data/spec/unit/thinking_sphinx/index/builder_spec.rb +1 -29
  39. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +1 -39
  40. data/spec/unit/thinking_sphinx/index_spec.rb +78 -226
  41. data/spec/unit/thinking_sphinx/search_spec.rb +29 -228
  42. data/spec/unit/thinking_sphinx_spec.rb +23 -19
  43. data/tasks/distribution.rb +48 -0
  44. data/tasks/rails.rake +1 -0
  45. data/tasks/testing.rb +86 -0
  46. data/vendor/after_commit/LICENSE +20 -0
  47. data/vendor/after_commit/README +16 -0
  48. data/vendor/after_commit/Rakefile +22 -0
  49. data/vendor/after_commit/init.rb +8 -0
  50. data/vendor/after_commit/lib/after_commit.rb +45 -0
  51. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  52. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  53. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  54. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  55. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  56. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  57. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  58. data/{lib → vendor/riddle/lib}/riddle.rb +9 -5
  59. data/{lib → vendor/riddle/lib}/riddle/client.rb +6 -26
  60. data/{lib → vendor/riddle/lib}/riddle/client/filter.rb +10 -1
  61. data/{lib → vendor/riddle/lib}/riddle/client/message.rb +0 -0
  62. data/{lib → vendor/riddle/lib}/riddle/client/response.rb +0 -0
  63. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  64. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  65. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  66. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  67. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  68. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  69. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  70. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  71. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  72. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  73. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  74. metadata +63 -10
  75. data/lib/test.rb +0 -46
  76. data/tasks/thinking_sphinx_tasks.rake +0 -1
  77. data/tasks/thinking_sphinx_tasks.rb +0 -86
@@ -0,0 +1,15 @@
1
+ require 'zlib'
2
+
3
+ module ThinkingSphinx
4
+ module Core
5
+ module String
6
+ def to_crc32
7
+ Zlib.crc32 self
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ class String
14
+ include ThinkingSphinx::Core::String
15
+ end
@@ -0,0 +1,27 @@
1
+ require 'thinking_sphinx/deltas/default_delta'
2
+ require 'thinking_sphinx/deltas/delayed_delta'
3
+ require 'thinking_sphinx/deltas/datetime_delta'
4
+
5
+ module ThinkingSphinx
6
+ module Deltas
7
+ def self.parse(index, options)
8
+ delta_option = options.delete(:delta)
9
+ case delta_option
10
+ when TrueClass, :default
11
+ DefaultDelta.new index, options
12
+ when :delayed
13
+ DelayedDelta.new index, options
14
+ when :datetime
15
+ DatetimeDelta.new index, options
16
+ when FalseClass, nil
17
+ nil
18
+ else
19
+ if delta_option.ancestors.include?(ThinkingSphinx::Deltas::DefaultDelta)
20
+ delta_option.new index, options
21
+ else
22
+ raise "Unknown delta type"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DatetimeDelta < ThinkingSphinx::Deltas::DefaultDelta
4
+ attr_accessor :column, :threshold
5
+
6
+ def initialize(index, options)
7
+ @index = index
8
+ @column = options.delete(:delta_column) || :updated_at
9
+ @threshold = options.delete(:threshold) || 1.day
10
+ end
11
+
12
+ def index(model, instance = nil)
13
+ # do nothing
14
+ true
15
+ end
16
+
17
+ def delayed_index(model)
18
+ config = ThinkingSphinx::Configuration.instance
19
+ rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : ""
20
+
21
+ output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}`
22
+ output += `#{config.bin_path}indexer --config #{config.config_file} #{rotate} --merge #{core_index_name model} #{delta_index_name model} --merge-dst-range sphinx_deleted 0 0`
23
+ puts output unless ThinkingSphinx.suppress_delta_output?
24
+
25
+ true
26
+ end
27
+
28
+ def toggle(instance)
29
+ # do nothing
30
+ end
31
+
32
+ def toggled(instance)
33
+ instance.send(@column) > @threshold.ago
34
+ end
35
+
36
+ def reset_query(model)
37
+ nil
38
+ end
39
+
40
+ def clause(model, toggled)
41
+ if toggled
42
+ "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
43
+ " > #{adapter.time_difference(@threshold)}"
44
+ else
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,67 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DefaultDelta
4
+ attr_accessor :column
5
+
6
+ def initialize(index, options)
7
+ @index = index
8
+ @column = options.delete(:delta_column) || :delta
9
+ end
10
+
11
+ def index(model, instance = nil)
12
+ return true unless ThinkingSphinx.updates_enabled? &&
13
+ ThinkingSphinx.deltas_enabled?
14
+ return true if instance && !toggled(instance)
15
+
16
+ config = ThinkingSphinx::Configuration.instance
17
+ client = Riddle::Client.new config.address, config.port
18
+ rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : ""
19
+
20
+ output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}`
21
+ puts(output) unless ThinkingSphinx.suppress_delta_output?
22
+
23
+ client.update(
24
+ core_index_name(model),
25
+ ['sphinx_deleted'],
26
+ {instance.sphinx_document_id => [1]}
27
+ ) if instance && ThinkingSphinx.sphinx_running? && instance.in_both_indexes?
28
+
29
+ true
30
+ end
31
+
32
+ def toggle(instance)
33
+ instance.delta = true
34
+ end
35
+
36
+ def toggled(instance)
37
+ instance.delta
38
+ end
39
+
40
+ def reset_query(model)
41
+ "UPDATE #{model.quoted_table_name} SET " +
42
+ "#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)}"
43
+ end
44
+
45
+ def clause(model, toggled)
46
+ "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
47
+ " = #{adapter.boolean(toggled)}"
48
+ end
49
+
50
+ protected
51
+
52
+ def core_index_name(model)
53
+ "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core"
54
+ end
55
+
56
+ def delta_index_name(model)
57
+ "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_delta"
58
+ end
59
+
60
+ private
61
+
62
+ def adapter
63
+ @adapter = @index.model.sphinx_database_adapter
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,25 @@
1
+ require 'delayed/job'
2
+
3
+ require 'thinking_sphinx/deltas/delayed_delta/delta_job'
4
+ require 'thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job'
5
+ require 'thinking_sphinx/deltas/delayed_delta/job'
6
+
7
+ module ThinkingSphinx
8
+ module Deltas
9
+ class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
10
+ def index(model, instance = nil)
11
+ ThinkingSphinx::Deltas::Job.enqueue(
12
+ ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model))
13
+ )
14
+
15
+ Delayed::Job.enqueue(
16
+ ThinkingSphinx::Deltas::FlagAsDeletedJob.new(
17
+ core_index_name(model), instance.sphinx_document_id
18
+ )
19
+ ) if instance
20
+
21
+ true
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DeltaJob
4
+ attr_accessor :index
5
+
6
+ def initialize(index)
7
+ @index = index
8
+ end
9
+
10
+ def perform
11
+ return true unless ThinkingSphinx.updates_enabled? &&
12
+ ThinkingSphinx.deltas_enabled?
13
+
14
+ config = ThinkingSphinx::Configuration.instance
15
+ client = Riddle::Client.new config.address, config.port
16
+
17
+ output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{index}`
18
+ puts output unless ThinkingSphinx.suppress_delta_output?
19
+
20
+ true
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class FlagAsDeletedJob
4
+ attr_accessor :index, :document_id
5
+
6
+ def initialize(index, document_id)
7
+ @index, @document_id = index, document_id
8
+ end
9
+
10
+ def perform
11
+ return true unless ThinkingSphinx.updates_enabled?
12
+
13
+ config = ThinkingSphinx::Configuration.instance
14
+ client = Riddle::Client.new config.address, config.port
15
+
16
+ client.update(
17
+ @index,
18
+ ['sphinx_deleted'],
19
+ {@document_id => [1]}
20
+ ) if ThinkingSphinx.sphinx_running? &&
21
+ ThinkingSphinx::Search.search_for_id(@document_id, @index)
22
+
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class Job < Delayed::Job
4
+ def self.enqueue(object, priority = 0)
5
+ super unless duplicates_exist(object)
6
+ end
7
+
8
+ def self.cancel_thinking_sphinx_jobs
9
+ if connection.tables.include?("delayed_jobs")
10
+ delete_all("handler LIKE '--- !ruby/object:ThinkingSphinx::Deltas::%'")
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def self.duplicates_exist(object)
17
+ count(
18
+ :conditions => {
19
+ :handler => object.to_yaml,
20
+ :locked_at => nil
21
+ }
22
+ ) > 0
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,58 @@
1
+ module ThinkingSphinx
2
+ class Facet
3
+ attr_reader :reference
4
+
5
+ def initialize(reference)
6
+ @reference = reference
7
+
8
+ if reference.columns.length != 1
9
+ raise "Can't translate Facets on multiple-column field or attribute"
10
+ end
11
+ end
12
+
13
+ def name
14
+ reference.unique_name
15
+ end
16
+
17
+ def attribute_name
18
+ # @attribute_name ||= case @reference
19
+ # when Attribute
20
+ # @reference.unique_name.to_s
21
+ # when Field
22
+ @attribute_name ||= @reference.unique_name.to_s + "_facet"
23
+ # end
24
+ end
25
+
26
+ def value(object, attribute_value)
27
+ return translate(object, attribute_value) if @reference.is_a?(Field)
28
+
29
+ case @reference.type
30
+ when :string
31
+ translate(object, attribute_value)
32
+ when :datetime
33
+ Time.at(attribute_value)
34
+ when :boolean
35
+ attribute_value > 0
36
+ else
37
+ attribute_value
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ name
43
+ end
44
+
45
+ private
46
+
47
+ def translate(object, attribute_value)
48
+ column.__stack.each { |method|
49
+ object = object.send(method)
50
+ }
51
+ object.send(column.__name)
52
+ end
53
+
54
+ def column
55
+ @reference.columns.first
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,60 @@
1
+ module ThinkingSphinx
2
+ class FacetCollection < Hash
3
+ attr_accessor :arguments
4
+
5
+ def initialize(arguments)
6
+ @arguments = arguments.clone
7
+ @attribute_values = {}
8
+ @facets = []
9
+ end
10
+
11
+ def add_from_results(facet, results)
12
+ facet = facet_from_object(results.first, facet) if facet.is_a?(String)
13
+
14
+ self[facet.name] ||= {}
15
+ @attribute_values[facet.name] ||= {}
16
+ @facets << facet
17
+
18
+ results.each_with_groupby_and_count { |result, group, count|
19
+ facet_value = facet.value(result, group)
20
+
21
+ self[facet.name][facet_value] ||= 0
22
+ self[facet.name][facet_value] += count
23
+ @attribute_values[facet.name][facet_value] = group
24
+ }
25
+ end
26
+
27
+ def for(hash = {})
28
+ arguments = @arguments.clone
29
+ options = arguments.extract_options!
30
+ options[:with] ||= {}
31
+
32
+ hash.each do |key, value|
33
+ attrib = facet_for_key(key).attribute_name
34
+ options[:with][attrib] = underlying_value key, value
35
+ end
36
+
37
+ arguments << options
38
+ ThinkingSphinx::Search.search *arguments
39
+ end
40
+
41
+ private
42
+
43
+ def underlying_value(key, value)
44
+ case value
45
+ when Array
46
+ value.collect { |item| underlying_value(key, item) }
47
+ else
48
+ @attribute_values[key][value]
49
+ end
50
+ end
51
+
52
+ def facet_for_key(key)
53
+ @facets.detect { |facet| facet.name == key }
54
+ end
55
+
56
+ def facet_from_object(object, name)
57
+ object.sphinx_facets.detect { |facet| facet.attribute_name == name }
58
+ end
59
+ end
60
+ end
@@ -8,7 +8,8 @@ module ThinkingSphinx
8
8
  # associations. Which can get messy. Use Index.link!, it really helps.
9
9
  #
10
10
  class Field
11
- attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes, :prefixes
11
+ attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes,
12
+ :prefixes, :faceted
12
13
 
13
14
  # To create a new field, you'll need to pass in either a single Column
14
15
  # or an array of them, and some (optional) options. The columns are
@@ -58,10 +59,11 @@ module ThinkingSphinx
58
59
 
59
60
  raise "Cannot define a field with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }
60
61
 
61
- @alias = options[:as]
62
- @sortable = options[:sortable] || false
63
- @infixes = options[:infixes] || false
64
- @prefixes = options[:prefixes] || false
62
+ @alias = options[:as]
63
+ @sortable = options[:sortable] || false
64
+ @infixes = options[:infixes] || false
65
+ @prefixes = options[:prefixes] || false
66
+ @faceted = options[:facet] || false
65
67
  end
66
68
 
67
69
  # Get the part of the SELECT clause related to this field. Don't forget
@@ -75,10 +77,10 @@ module ThinkingSphinx
75
77
  column_with_prefix(column)
76
78
  }.join(', ')
77
79
 
78
- clause = concatenate(clause) if concat_ws?
79
- clause = group_concatenate(clause) if is_many?
80
+ clause = adapter.concatenate(clause) if concat_ws?
81
+ clause = adapter.group_concatenate(clause) if is_many?
80
82
 
81
- "#{cast_to_string clause } AS #{quote_column(unique_name)}"
83
+ "#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}"
82
84
  end
83
85
 
84
86
  # Get the part of the GROUP BY clause related to this field - if one is
@@ -110,39 +112,16 @@ module ThinkingSphinx
110
112
  end
111
113
  end
112
114
 
113
- private
114
-
115
- def concatenate(clause)
116
- case @model.connection.class.name
117
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
118
- "CONCAT_WS(' ', #{clause})"
119
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
120
- clause.split(', ').join(" || ' ' || ")
121
- else
122
- clause
123
- end
115
+ def to_facet
116
+ return nil unless @faceted
117
+
118
+ ThinkingSphinx::Facet.new(self)
124
119
  end
125
120
 
126
- def group_concatenate(clause)
127
- case @model.connection.class.name
128
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
129
- "GROUP_CONCAT(#{clause} SEPARATOR ' ')"
130
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
131
- "array_to_string(array_accum(#{clause}), ' ')"
132
- else
133
- clause
134
- end
135
- end
121
+ private
136
122
 
137
- def cast_to_string(clause)
138
- case @model.connection.class.name
139
- when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
140
- "CAST(#{clause} AS CHAR)"
141
- when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
142
- clause
143
- else
144
- clause
145
- end
123
+ def adapter
124
+ @adapter ||= @model.sphinx_database_adapter
146
125
  end
147
126
 
148
127
  def quote_column(column)
@@ -156,16 +135,7 @@ module ThinkingSphinx
156
135
  def concat_ws?
157
136
  @columns.length > 1 || multiple_associations?
158
137
  end
159
-
160
- # Checks the association tree for each column - if they're all the same,
161
- # returns false.
162
- #
163
- def multiple_sources?
164
- first = associations[@columns.first]
165
-
166
- !@columns.all? { |col| associations[col] == first }
167
- end
168
-
138
+
169
139
  # Checks whether any column requires multiple associations (which only
170
140
  # happens for polymorphic situations).
171
141
  #
@@ -198,9 +168,5 @@ module ThinkingSphinx
198
168
  def is_many?
199
169
  associations.values.flatten.any? { |assoc| assoc.is_many? }
200
170
  end
201
-
202
- def is_string?
203
- columns.all? { |col| col.is_string? }
204
- end
205
171
  end
206
172
  end