dpickett-thinking-sphinx 1.1.4 → 1.1.12

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