dpickett-thinking-sphinx 1.1.4 → 1.1.12

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 (44) hide show
  1. data/README.textile +126 -0
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +14 -1
  4. data/lib/thinking_sphinx/active_record.rb +23 -5
  5. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +9 -1
  6. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +3 -2
  7. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +4 -3
  8. data/lib/thinking_sphinx/association.rb +17 -0
  9. data/lib/thinking_sphinx/attribute.rb +106 -95
  10. data/lib/thinking_sphinx/class_facet.rb +0 -5
  11. data/lib/thinking_sphinx/collection.rb +7 -1
  12. data/lib/thinking_sphinx/configuration.rb +9 -4
  13. data/lib/thinking_sphinx/core/string.rb +3 -10
  14. data/lib/thinking_sphinx/deltas/default_delta.rb +8 -5
  15. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  16. data/lib/thinking_sphinx/deltas.rb +7 -2
  17. data/lib/thinking_sphinx/deploy/capistrano.rb +80 -0
  18. data/lib/thinking_sphinx/facet.rb +22 -9
  19. data/lib/thinking_sphinx/facet_collection.rb +27 -12
  20. data/lib/thinking_sphinx/field.rb +4 -96
  21. data/lib/thinking_sphinx/index/builder.rb +46 -15
  22. data/lib/thinking_sphinx/index.rb +58 -66
  23. data/lib/thinking_sphinx/property.rb +133 -0
  24. data/lib/thinking_sphinx/rails_additions.rb +7 -4
  25. data/lib/thinking_sphinx/search.rb +181 -44
  26. data/lib/thinking_sphinx/tasks.rb +4 -4
  27. data/lib/thinking_sphinx.rb +47 -11
  28. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +2 -2
  29. data/spec/unit/thinking_sphinx/active_record_spec.rb +64 -4
  30. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -1
  31. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
  32. data/spec/unit/thinking_sphinx/facet_spec.rb +46 -0
  33. data/spec/unit/thinking_sphinx/index_spec.rb +90 -0
  34. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
  35. data/spec/unit/thinking_sphinx/search_spec.rb +44 -0
  36. data/spec/unit/thinking_sphinx_spec.rb +10 -6
  37. data/tasks/distribution.rb +1 -1
  38. data/tasks/testing.rb +7 -15
  39. data/vendor/after_commit/init.rb +3 -0
  40. data/vendor/after_commit/lib/after_commit/active_record.rb +27 -4
  41. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +1 -1
  42. data/vendor/after_commit/lib/after_commit.rb +4 -1
  43. metadata +12 -3
  44. data/README +0 -107
@@ -31,8 +31,10 @@ module ThinkingSphinx
31
31
  # fashion to database.yml - using the following keys: config_file,
32
32
  # searchd_log_file, query_log_file, pid_file, searchd_file_path, port,
33
33
  # allow_star, enable_star, min_prefix_len, min_infix_len, mem_limit,
34
- # max_matches, # morphology, charset_type, charset_table, ignore_chars,
35
- # html_strip, # html_remove_elements. I think you've got the idea.
34
+ # max_matches, morphology, charset_type, charset_table, ignore_chars,
35
+ # html_strip, html_remove_elements, delayed_job_priority.
36
+ #
37
+ # I think you've got the idea.
36
38
  #
37
39
  # Each setting in the YAML file is optional - so only put in the ones you
38
40
  # want to change.
@@ -55,7 +57,8 @@ module ThinkingSphinx
55
57
 
56
58
  attr_accessor :config_file, :searchd_log_file, :query_log_file,
57
59
  :pid_file, :searchd_file_path, :address, :port, :allow_star,
58
- :database_yml_file, :app_root, :bin_path, :model_directories
60
+ :database_yml_file, :app_root, :bin_path, :model_directories,
61
+ :delayed_job_priority
59
62
 
60
63
  attr_accessor :source_options, :index_options
61
64
 
@@ -85,7 +88,9 @@ module ThinkingSphinx
85
88
  self.searchd_file_path = "#{self.app_root}/db/sphinx/#{environment}"
86
89
  self.allow_star = false
87
90
  self.bin_path = ""
88
- self.model_directories = ["#{app_root}/app/models/"]
91
+ self.model_directories = ["#{app_root}/app/models/"] +
92
+ Dir.glob("#{app_root}/vendor/plugins/*/app/models/")
93
+ self.delayed_job_priority = 0
89
94
 
90
95
  self.source_options = {}
91
96
  self.index_options = {
@@ -1,18 +1,11 @@
1
+ require 'zlib'
2
+
1
3
  module ThinkingSphinx
2
4
  module Core
3
5
  module String
4
-
5
6
  def to_crc32
6
- result = 0xFFFFFFFF
7
- self.each_byte do |byte|
8
- result ^= byte
9
- 8.times do
10
- result = (result >> 1) ^ (0xEDB88320 * (result & 1))
11
- end
12
- end
13
- result ^ 0xFFFFFFFF
7
+ Zlib.crc32 self
14
8
  end
15
-
16
9
  end
17
10
  end
18
11
  end
@@ -11,18 +11,20 @@ module ThinkingSphinx
11
11
  def index(model, instance = nil)
12
12
  return true unless ThinkingSphinx.updates_enabled? &&
13
13
  ThinkingSphinx.deltas_enabled?
14
+ return true if instance && !toggled(instance)
14
15
 
15
16
  config = ThinkingSphinx::Configuration.instance
16
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?
17
22
 
18
23
  client.update(
19
24
  core_index_name(model),
20
25
  ['sphinx_deleted'],
21
26
  {instance.sphinx_document_id => [1]}
22
- ) if instance && ThinkingSphinx.sphinx_running? && instance.in_core_index?
23
-
24
- output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{delta_index_name model}`
25
- puts output unless ThinkingSphinx.suppress_delta_output?
27
+ ) if instance && ThinkingSphinx.sphinx_running? && instance.in_both_indexes?
26
28
 
27
29
  true
28
30
  end
@@ -37,7 +39,8 @@ module ThinkingSphinx
37
39
 
38
40
  def reset_query(model)
39
41
  "UPDATE #{model.quoted_table_name} SET " +
40
- "#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)}"
42
+ "#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)} " +
43
+ "WHERE #{@index.quote_column(@column.to_s)} = #{adapter.boolean(true)}"
41
44
  end
42
45
 
43
46
  def clause(model, toggled)
@@ -9,13 +9,15 @@ module ThinkingSphinx
9
9
  class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
10
10
  def index(model, instance = nil)
11
11
  ThinkingSphinx::Deltas::Job.enqueue(
12
- ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model))
12
+ ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model)),
13
+ ThinkingSphinx::Configuration.instance.delayed_job_priority
13
14
  )
14
15
 
15
16
  Delayed::Job.enqueue(
16
17
  ThinkingSphinx::Deltas::FlagAsDeletedJob.new(
17
18
  core_index_name(model), instance.sphinx_document_id
18
- )
19
+ ),
20
+ ThinkingSphinx::Configuration.instance.delayed_job_priority
19
21
  ) if instance
20
22
 
21
23
  true
@@ -5,7 +5,8 @@ require 'thinking_sphinx/deltas/datetime_delta'
5
5
  module ThinkingSphinx
6
6
  module Deltas
7
7
  def self.parse(index, options)
8
- case options.delete(:delta)
8
+ delta_option = options.delete(:delta)
9
+ case delta_option
9
10
  when TrueClass, :default
10
11
  DefaultDelta.new index, options
11
12
  when :delayed
@@ -15,7 +16,11 @@ module ThinkingSphinx
15
16
  when FalseClass, nil
16
17
  nil
17
18
  else
18
- raise "Unknown delta type"
19
+ if delta_option.ancestors.include?(ThinkingSphinx::Deltas::DefaultDelta)
20
+ delta_option.new index, options
21
+ else
22
+ raise "Unknown delta type"
23
+ end
19
24
  end
20
25
  end
21
26
  end
@@ -0,0 +1,80 @@
1
+ namespace :thinking_sphinx do
2
+ namespace :install do
3
+ desc "Install Sphinx by source"
4
+ task :sphinx do
5
+ with_postgres = false
6
+ run "which pg_config" do |channel, stream, data|
7
+ with_postgres = !(data.nil? || data == "")
8
+ end
9
+
10
+ args = []
11
+ if with_postgres
12
+ run "pg_config --pkgincludedir" do |channel, stream, data|
13
+ args << "--with-pgsql=#{data}"
14
+ end
15
+ end
16
+
17
+ commands = <<-CMD
18
+ wget -q http://www.sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz >> sphinx.log
19
+ tar xzvf sphinx-0.9.8.1.tar.gz
20
+ cd sphinx-0.9.8.1
21
+ ./configure #{args.join(" ")}
22
+ make
23
+ sudo make install
24
+ rm -rf sphinx-0.9.8.1 sphinx-0.9.8.1.tar.gz
25
+ CMD
26
+ run commands.split(/\n\s+/).join(" && ")
27
+ end
28
+
29
+ desc "Install Thinking Sphinx as a gem from GitHub"
30
+ task :ts do
31
+ sudo "gem install freelancing-god-thinking-sphinx --source http://gems.github.com"
32
+ end
33
+ end
34
+
35
+ desc "Generate the Sphinx configuration file"
36
+ task :configure do
37
+ rake "thinking_sphinx:configure"
38
+ end
39
+
40
+ desc "Index data"
41
+ task :index do
42
+ rake "thinking_sphinx:index"
43
+ end
44
+
45
+ desc "Start the Sphinx daemon"
46
+ task :start do
47
+ configure
48
+ rake "thinking_sphinx:start"
49
+ end
50
+
51
+ desc "Stop the Sphinx daemon"
52
+ task :stop do
53
+ configure
54
+ rake "thinking_sphinx:stop"
55
+ end
56
+
57
+ desc "Stop and then start the Sphinx daemon"
58
+ task :restart do
59
+ stop
60
+ start
61
+ end
62
+
63
+ desc "Stop, re-index and then start the Sphinx daemon"
64
+ task :rebuild do
65
+ stop
66
+ index
67
+ start
68
+ end
69
+
70
+ desc "Add the shared folder for sphinx files for the production environment"
71
+ task :shared_sphinx_folder, :roles => :web do
72
+ sudo "mkdir -p #{shared_path}/db/sphinx/production"
73
+ end
74
+
75
+ def rake(*tasks)
76
+ tasks.each do |t|
77
+ run "cd #{current_path} && rake #{t} RAILS_ENV=production"
78
+ end
79
+ end
80
+ end
@@ -9,25 +9,38 @@ module ThinkingSphinx
9
9
  raise "Can't translate Facets on multiple-column field or attribute"
10
10
  end
11
11
  end
12
+
13
+ def self.name_for(facet)
14
+ case facet
15
+ when Facet
16
+ facet.name
17
+ when String, Symbol
18
+ facet.to_s.gsub(/(_facet|_crc)$/,'').to_sym
19
+ end
20
+ end
12
21
 
13
22
  def name
14
23
  reference.unique_name
15
24
  end
25
+
26
+ def self.attribute_name_for(name)
27
+ name.to_s == 'class' ? 'class_crc' : "#{name}_facet"
28
+ end
16
29
 
17
30
  def attribute_name
18
- @attribute_name ||= case @reference
19
- when Attribute
20
- @reference.unique_name.to_s
21
- when Field
22
- @reference.unique_name.to_s + "_sort"
23
- end
31
+ # @attribute_name ||= case @reference
32
+ # when Attribute
33
+ # @reference.unique_name.to_s
34
+ # when Field
35
+ @attribute_name ||= @reference.unique_name.to_s + "_facet"
36
+ # end
24
37
  end
25
38
 
26
39
  def value(object, attribute_value)
27
40
  return translate(object, attribute_value) if @reference.is_a?(Field)
28
41
 
29
42
  case @reference.type
30
- when :string, :multi
43
+ when :string
31
44
  translate(object, attribute_value)
32
45
  when :datetime
33
46
  Time.at(attribute_value)
@@ -42,7 +55,7 @@ module ThinkingSphinx
42
55
  name
43
56
  end
44
57
 
45
- protected
58
+ private
46
59
 
47
60
  def translate(object, attribute_value)
48
61
  column.__stack.each { |method|
@@ -55,4 +68,4 @@ module ThinkingSphinx
55
68
  @reference.columns.first
56
69
  end
57
70
  end
58
- end
71
+ end
@@ -5,20 +5,26 @@ module ThinkingSphinx
5
5
  def initialize(arguments)
6
6
  @arguments = arguments.clone
7
7
  @attribute_values = {}
8
- @facets = []
8
+ @facet_names = []
9
9
  end
10
10
 
11
11
  def add_from_results(facet, results)
12
- self[facet.name] ||= {}
13
- @attribute_values[facet.name] = {}
14
- @facets << facet
12
+ name = ThinkingSphinx::Facet.name_for(facet)
13
+
14
+ self[name] ||= {}
15
+ @attribute_values[name] ||= {}
16
+ @facet_names << name
17
+
18
+ return if results.empty?
19
+
20
+ facet = facet_from_object(results.first, facet) if facet.is_a?(String)
15
21
 
16
22
  results.each_with_groupby_and_count { |result, group, count|
17
23
  facet_value = facet.value(result, group)
18
24
 
19
- self[facet.name][facet_value] ||= 0
20
- self[facet.name][facet_value] += count
21
- @attribute_values[facet.name][facet_value] ||= group
25
+ self[name][facet_value] ||= 0
26
+ self[name][facet_value] += count
27
+ @attribute_values[name][facet_value] = group
22
28
  }
23
29
  end
24
30
 
@@ -28,8 +34,8 @@ module ThinkingSphinx
28
34
  options[:with] ||= {}
29
35
 
30
36
  hash.each do |key, value|
31
- attrib = facet_for_key(key).attribute_name
32
- options[:with][attrib] = @attribute_values[key][value]
37
+ attrib = ThinkingSphinx::Facet.attribute_name_for(key)
38
+ options[:with][attrib] = underlying_value key, value
33
39
  end
34
40
 
35
41
  arguments << options
@@ -38,8 +44,17 @@ module ThinkingSphinx
38
44
 
39
45
  private
40
46
 
41
- def facet_for_key(key)
42
- @facets.detect { |facet| facet.name == key }
47
+ def underlying_value(key, value)
48
+ case value
49
+ when Array
50
+ value.collect { |item| underlying_value(key, item) }
51
+ else
52
+ @attribute_values[key][value]
53
+ end
54
+ end
55
+
56
+ def facet_from_object(object, name)
57
+ object.sphinx_facets.detect { |facet| facet.attribute_name == name }
43
58
  end
44
59
  end
45
- end
60
+ end
@@ -7,9 +7,8 @@ module ThinkingSphinx
7
7
  # generate SQL statements, you'll need to set the base model, and all the
8
8
  # associations. Which can get messy. Use Index.link!, it really helps.
9
9
  #
10
- class Field
11
- attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes,
12
- :prefixes, :faceted
10
+ class Field < ThinkingSphinx::Property
11
+ attr_accessor :sortable, :infixes, :prefixes
13
12
 
14
13
  # To create a new field, you'll need to pass in either a single Column
15
14
  # or an array of them, and some (optional) options. The columns are
@@ -54,16 +53,11 @@ module ThinkingSphinx
54
53
  # )
55
54
  #
56
55
  def initialize(columns, options = {})
57
- @columns = Array(columns)
58
- @associations = {}
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) }
56
+ super
61
57
 
62
- @alias = options[:as]
63
58
  @sortable = options[:sortable] || false
64
59
  @infixes = options[:infixes] || false
65
60
  @prefixes = options[:prefixes] || false
66
- @faceted = options[:facet] || false
67
61
  end
68
62
 
69
63
  # Get the part of the SELECT clause related to this field. Don't forget
@@ -77,96 +71,10 @@ module ThinkingSphinx
77
71
  column_with_prefix(column)
78
72
  }.join(', ')
79
73
 
80
- clause = adapter.concatenate(clause) if concat_ws?
74
+ clause = adapter.concatenate(clause) if concat_ws?
81
75
  clause = adapter.group_concatenate(clause) if is_many?
82
76
 
83
77
  "#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}"
84
78
  end
85
-
86
- # Get the part of the GROUP BY clause related to this field - if one is
87
- # needed. If not, all you'll get back is nil. The latter will happen if
88
- # there's multiple data values (read: a has_many or has_and_belongs_to_many
89
- # association).
90
- #
91
- def to_group_sql
92
- case
93
- when is_many?, ThinkingSphinx.use_group_by_shortcut?
94
- nil
95
- else
96
- @columns.collect { |column|
97
- column_with_prefix(column)
98
- }
99
- end
100
- end
101
-
102
- # Returns the unique name of the field - which is either the alias of
103
- # the field, or the name of the only column - if there is only one. If
104
- # there isn't, there should be an alias. Else things probably won't work.
105
- # Consider yourself warned.
106
- #
107
- def unique_name
108
- if @columns.length == 1
109
- @alias || @columns.first.__name
110
- else
111
- @alias
112
- end
113
- end
114
-
115
- def to_facet
116
- return nil unless @faceted
117
-
118
- ThinkingSphinx::Facet.new(self)
119
- end
120
-
121
- private
122
-
123
- def adapter
124
- @adapter ||= @model.sphinx_database_adapter
125
- end
126
-
127
- def quote_column(column)
128
- @model.connection.quote_column_name(column)
129
- end
130
-
131
- # Indication of whether the columns should be concatenated with a space
132
- # between each value. True if there's either multiple sources or multiple
133
- # associations.
134
- #
135
- def concat_ws?
136
- @columns.length > 1 || multiple_associations?
137
- end
138
-
139
- # Checks whether any column requires multiple associations (which only
140
- # happens for polymorphic situations).
141
- #
142
- def multiple_associations?
143
- associations.any? { |col,assocs| assocs.length > 1 }
144
- end
145
-
146
- # Builds a column reference tied to the appropriate associations. This
147
- # dives into the associations hash and their corresponding joins to
148
- # figure out how to correctly reference a column in SQL.
149
- #
150
- def column_with_prefix(column)
151
- if column.is_string?
152
- column.__name
153
- elsif associations[column].empty?
154
- "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
155
- else
156
- associations[column].collect { |assoc|
157
- assoc.has_column?(column.__name) ?
158
- "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
159
- ".#{quote_column(column.__name)}" :
160
- nil
161
- }.compact.join(', ')
162
- end
163
- end
164
-
165
- # Could there be more than one value related to the parent record? If so,
166
- # then this will return true. If not, false. It's that simple.
167
- #
168
- def is_many?
169
- associations.values.flatten.any? { |assoc| assoc.is_many? }
170
- end
171
79
  end
172
80
  end
@@ -87,22 +87,16 @@ module ThinkingSphinx
87
87
  def indexes(*args)
88
88
  options = args.extract_options!
89
89
  args.each do |columns|
90
- fields << Field.new(FauxColumn.coerce(columns), options)
90
+ field = Field.new(FauxColumn.coerce(columns), options)
91
+ fields << field
91
92
 
92
- if fields.last.sortable || fields.last.faceted
93
- attributes << Attribute.new(
94
- fields.last.columns.collect { |col| col.clone },
95
- options.merge(
96
- :type => :string,
97
- :as => fields.last.unique_name.to_s.concat("_sort").to_sym
98
- ).except(:facet)
99
- )
100
- end
93
+ add_sort_attribute field, options if field.sortable
94
+ add_facet_attribute field, options if field.faceted
101
95
  end
102
96
  end
103
97
  alias_method :field, :indexes
104
98
  alias_method :includes, :indexes
105
-
99
+
106
100
  # This is the method to add attributes to your index (hence why it is
107
101
  # aliased as 'attribute'). The syntax is the same as #indexes, so use
108
102
  # that as starting point, but keep in mind the following points.
@@ -142,7 +136,10 @@ module ThinkingSphinx
142
136
  def has(*args)
143
137
  options = args.extract_options!
144
138
  args.each do |columns|
145
- attributes << Attribute.new(FauxColumn.coerce(columns), options)
139
+ attribute = Attribute.new(FauxColumn.coerce(columns), options)
140
+ attributes << attribute
141
+
142
+ add_facet_attribute attribute, options if attribute.faceted
146
143
  end
147
144
  end
148
145
  alias_method :attribute, :has
@@ -152,7 +149,10 @@ module ThinkingSphinx
152
149
  options[:facet] = true
153
150
 
154
151
  args.each do |columns|
155
- attributes << Attribute.new(FauxColumn.coerce(columns), options)
152
+ attribute = Attribute.new(FauxColumn.coerce(columns), options)
153
+ attributes << attribute
154
+
155
+ add_facet_attribute attribute, options
156
156
  end
157
157
  end
158
158
 
@@ -200,7 +200,17 @@ module ThinkingSphinx
200
200
  #
201
201
  # Please don't forget to add a boolean field named 'delta' to your
202
202
  # model's database table if enabling the delta index for it.
203
+ # Valid options for the delta property are:
203
204
  #
205
+ # true
206
+ # false
207
+ # :default
208
+ # :delayed
209
+ # :datetime
210
+ #
211
+ # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
212
+ # your own handling for delta indexing.
213
+
204
214
  def set_property(*args)
205
215
  options = args.extract_options!
206
216
  if options.empty?
@@ -224,8 +234,29 @@ module ThinkingSphinx
224
234
  #
225
235
  # Example: indexes assoc(:properties).column
226
236
  #
227
- def assoc(assoc)
228
- FauxColumn.new(method)
237
+ def assoc(assoc, *args)
238
+ FauxColumn.new(assoc, *args)
239
+ end
240
+
241
+ private
242
+
243
+ def add_sort_attribute(field, options)
244
+ add_internal_attribute field, options, "_sort"
245
+ end
246
+
247
+ def add_facet_attribute(resource, options)
248
+ add_internal_attribute resource, options, "_facet", true
249
+ end
250
+
251
+ def add_internal_attribute(resource, options, suffix, crc = false)
252
+ @attributes << Attribute.new(
253
+ resource.columns.collect { |col| col.clone },
254
+ options.merge(
255
+ :type => resource.is_a?(Field) ? :string : options[:type],
256
+ :as => resource.unique_name.to_s.concat(suffix).to_sym,
257
+ :crc => crc
258
+ ).except(:facet)
259
+ )
229
260
  end
230
261
  end
231
262
  end