freelancing-god-thinking-sphinx 0.9.6 → 0.9.7

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.
data/README CHANGED
@@ -4,6 +4,26 @@
4
4
 
5
5
  First, if you haven't done so already, check out the main usage[http://ts.freelancing-gods.com/usage.html] page. Once you've done that, the next place to look for information is the specific method docs - ThinkingSphinx::Search and ThinkingSphinx::Index::Builder in particular.
6
6
 
7
+ == Contributing
8
+
9
+ Fork on GitHub and after you've committed tested patches, send a pull request.
10
+
11
+ To get the spec suite running, you will need to install the not-a-mock gem if you don't already have it:
12
+
13
+ git clone git://github.com/freelancing-god/not-a-mock.git
14
+ cd not-a-mock
15
+ rake gem
16
+ gem install pkg/not_a_mock-1.1.0.gem
17
+
18
+ Then set up your database
19
+
20
+ cp spec/fixtures/database.yml.default spec/fixtures/database.yml
21
+ mysqladmin -u root create thinking_sphinx
22
+
23
+ You should now have a passing test suite from which to build your patch on.
24
+
25
+ rake spec
26
+
7
27
  == Contributors
8
28
 
9
29
  Since I first released this library, there's been quite a few people who have submitted patches, to my immense gratitude. Others have suggested syntax changes and general improvements. So my thanks to the following people:
@@ -22,4 +42,6 @@ Since I first released this library, there's been quite a few people who have su
22
42
  - Patrick Lenz
23
43
  - Björn Andreasson
24
44
  - James Healy
25
- - Jae-Jun Hwang
45
+ - Jae-Jun Hwang
46
+ - Xavier Shay
47
+ - Jason Rust
@@ -20,7 +20,7 @@ module ThinkingSphinx
20
20
  module Version #:nodoc:
21
21
  Major = 0
22
22
  Minor = 9
23
- Tiny = 6
23
+ Tiny = 7
24
24
 
25
25
  String = [Major, Minor, Tiny].join('.')
26
26
  end
@@ -67,6 +67,9 @@ module ThinkingSphinx
67
67
  @@deltas_enabled = value
68
68
  end
69
69
 
70
+ # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
71
+ # or if not using MySQL, this will return false.
72
+ #
70
73
  def self.use_group_by_shortcut?
71
74
  ::ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") &&
72
75
  ::ActiveRecord::Base.connection.is_a?(
@@ -78,6 +78,8 @@ module ThinkingSphinx
78
78
  after_commit :index_delta
79
79
  end
80
80
 
81
+ after_destroy :toggle_deleted
82
+
81
83
  index
82
84
  end
83
85
  alias_method :sphinx_index, :define_index
@@ -111,5 +113,22 @@ module ThinkingSphinx
111
113
  :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
112
114
  )
113
115
  end
116
+
117
+ def toggle_deleted
118
+ config = ThinkingSphinx::Configuration.new
119
+ client = Riddle::Client.new config.address, config.port
120
+
121
+ client.update(
122
+ "#{self.class.name.downcase}_core",
123
+ ['sphinx_deleted'],
124
+ {self.id => 1}
125
+ )
126
+
127
+ client.update(
128
+ "#{self.class.name.downcase}_delta",
129
+ ['sphinx_deleted'],
130
+ {self.id => 1}
131
+ ) if self.class.indexes.any? { |index| index.delta? }
132
+ end
114
133
  end
115
134
  end
@@ -27,6 +27,10 @@ module ThinkingSphinx
27
27
  end
28
28
  end
29
29
 
30
+ def after_commit
31
+ # Deliberately blank.
32
+ end
33
+
30
34
  # Normal boolean save wrapped in a handler for the after_commit
31
35
  # callback.
32
36
  #
@@ -122,16 +122,16 @@ module ThinkingSphinx
122
122
  options[:class_name] = klass.name
123
123
  options[:foreign_key] ||= "#{ref.name}_id"
124
124
 
125
- foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
125
+ quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
126
126
  case options[:conditions]
127
127
  when nil
128
- options[:conditions] = "::ts_join_alias::.#{foreign_type} = '#{klass.name}'"
128
+ options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
129
129
  when Array
130
- options[:conditions] << "::ts_join_alias::.#{foreign_type} = '#{klass.name}'"
130
+ options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
131
131
  when Hash
132
- options[:conditions].merge!(foreign_type => klass.name)
132
+ options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
133
133
  else
134
- options[:conditions] << " AND ::ts_join_alias::.#{foreign_type} = '#{klass.name}'"
134
+ options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
135
135
  end
136
136
 
137
137
  options
@@ -57,6 +57,8 @@ module ThinkingSphinx
57
57
  @columns = Array(columns)
58
58
  @associations = {}
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) }
61
+
60
62
  @alias = options[:as]
61
63
  @type = options[:type]
62
64
  end
@@ -139,7 +141,7 @@ module ThinkingSphinx
139
141
  when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
140
142
  "CONCAT_WS('#{separator}', #{clause})"
141
143
  when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
142
- clause.split(', ').join(" || #{separator} || ")
144
+ clause.split(', ').join(" || '#{separator}' || ")
143
145
  else
144
146
  clause
145
147
  end
@@ -271,7 +273,8 @@ module ThinkingSphinx
271
273
  klasses = @associations[col].empty? ? [@model] :
272
274
  @associations[col].collect { |assoc| assoc.reflection.klass }
273
275
  klasses.all? { |klass|
274
- klass.columns.detect { |column| column.name == col.__name.to_s }.type == :integer
276
+ column = klass.columns.detect { |column| column.name == col.__name.to_s }
277
+ !column.nil? && column.type == :integer
275
278
  }
276
279
  }
277
280
  end
@@ -49,11 +49,12 @@ module ThinkingSphinx
49
49
  self.app_root = Merb.root if defined?(Merb)
50
50
  self.app_root ||= app_root
51
51
 
52
- self.config_file = "#{app_root}/config/#{environment}.sphinx.conf"
53
- self.searchd_log_file = "#{app_root}/log/searchd.log"
54
- self.query_log_file = "#{app_root}/log/searchd.query.log"
55
- self.pid_file = "#{app_root}/log/searchd.#{environment}.pid"
56
- self.searchd_file_path = "#{app_root}/db/sphinx/#{environment}"
52
+ self.config_file = "#{self.app_root}/config/#{environment}.sphinx.conf"
53
+ self.searchd_log_file = "#{self.app_root}/log/searchd.log"
54
+ self.query_log_file = "#{self.app_root}/log/searchd.query.log"
55
+ self.pid_file = "#{self.app_root}/log/searchd.#{environment}.pid"
56
+ self.searchd_file_path = "#{self.app_root}/db/sphinx/#{environment}"
57
+ self.address = "0.0.0.0"
57
58
  self.port = 3312
58
59
  self.allow_star = false
59
60
  self.mem_limit = "64M"
@@ -97,6 +98,7 @@ indexer
97
98
 
98
99
  searchd
99
100
  {
101
+ address = #{self.address}
100
102
  port = #{self.port}
101
103
  log = #{self.searchd_log_file}
102
104
  query_log = #{self.query_log_file}
@@ -110,107 +112,27 @@ searchd
110
112
  ThinkingSphinx.indexed_models.each do |model|
111
113
  model = model.constantize
112
114
  sources = []
115
+ delta_sources = []
113
116
  prefixed_fields = []
114
117
  infixed_fields = []
115
118
 
116
119
  model.indexes.each_with_index do |index, i|
117
- # Set up associations and joins
118
- index.link!
120
+ file.write index.to_config(i, database_conf, charset_type)
119
121
 
120
- attr_sources = index.attributes.collect { |attrib|
121
- attrib.to_sphinx_clause
122
- }.join("\n ")
123
-
124
- adapter = case index.adapter
125
- when :postgres
126
- create_array_accum
127
- "pgsql"
128
- when :mysql
129
- "mysql"
130
- else
131
- raise "Unsupported Database Adapter: Sphinx only supports MySQL and PosgreSQL"
132
- end
133
-
134
- file.write <<-SOURCE
135
-
136
- source #{model.name.downcase}_#{i}_core
137
- {
138
- type = #{adapter}
139
- sql_host = #{database_conf[:host] || "localhost"}
140
- sql_user = #{database_conf[:username]}
141
- sql_pass = #{database_conf[:password]}
142
- sql_db = #{database_conf[:database]}
143
-
144
- sql_query_pre = #{charset_type == "utf-8" && adapter == "mysql" ? "SET NAMES utf8" : ""}
145
- sql_query_pre = #{index.to_sql_query_pre}
146
- sql_query = #{index.to_sql.gsub(/\n/, ' ')}
147
- sql_query_range = #{index.to_sql_query_range}
148
- sql_query_info = #{index.to_sql_query_info}
149
- #{attr_sources}
150
- }
151
- SOURCE
152
-
153
- if index.delta?
154
- file.write <<-SOURCE
155
-
156
- source #{model.name.downcase}_#{i}_delta : #{model.name.downcase}_#{i}_core
157
- {
158
- sql_query_pre = #{charset_type == "utf-8" && adapter == "mysql" ? "SET NAMES utf8" : ""}
159
- sql_query = #{index.to_sql(:delta => true).gsub(/\n/, ' ')}
160
- sql_query_range = #{index.to_sql_query_range :delta => true}
161
- }
162
- SOURCE
163
- end
122
+ create_array_accum if index.adapter == :postgres
164
123
  sources << "#{model.name.downcase}_#{i}_core"
124
+ delta_sources << "#{model.name.downcase}_#{i}_delta" if index.delta?
165
125
  end
166
126
 
167
127
  source_list = sources.collect { |s| "source = #{s}" }.join("\n")
168
- delta_list = source_list.gsub(/_core$/, "_delta")
169
- file.write <<-INDEX
170
-
171
- index #{model.name.downcase}_core
172
- {
173
- #{source_list}
174
- path = #{self.searchd_file_path}/#{model.name.downcase}_core
175
- charset_type = #{self.charset_type}
176
- INDEX
177
- file.puts " morphology = #{self.morphology}" unless self.morphology.blank?
178
- file.puts " charset_table = #{self.charset_table}" unless self.charset_table.nil?
179
- file.puts " ignore_chars = #{self.ignore_chars}" unless self.ignore_chars.nil?
128
+ delta_list = delta_sources.collect { |s| "source = #{s}" }.join("\n")
180
129
 
181
- if self.allow_star
182
- file.puts " enable_star = 1"
183
- file.puts " min_prefix_len = 1"
130
+ file.write core_index_for_model(model, source_list)
131
+ unless delta_list.blank?
132
+ file.write delta_index_for_model(model, delta_list)
184
133
  end
185
134
 
186
- file.write("}\n")
187
-
188
- if model.indexes.any? { |index| index.delta? }
189
- file.write <<-INDEX
190
-
191
- index #{model.name.downcase}_delta : #{model.name.downcase}_core
192
- {
193
- #{delta_list}
194
- path = #{self.searchd_file_path}/#{model.name.downcase}_delta
195
- }
196
-
197
- index #{model.name.downcase}
198
- {
199
- type = distributed
200
- local = #{model.name.downcase}_core
201
- local = #{model.name.downcase}_delta
202
- charset_type = #{self.charset_type}
203
- }
204
- INDEX
205
- else
206
- file.write <<-INDEX
207
- index #{model.name.downcase}
208
- {
209
- type = distributed
210
- local = #{model.name.downcase}_core
211
- }
212
- INDEX
213
- end
135
+ file.write distributed_index_for_model(model)
214
136
  end
215
137
  end
216
138
  end
@@ -220,8 +142,9 @@ index #{model.name.downcase}
220
142
  # messy dependencies issues).
221
143
  #
222
144
  def load_models
223
- Dir["#{app_root}/app/models/**/*.rb"].each do |file|
224
- model_name = file.gsub(/^.*\/([\w_]+)\.rb/, '\1')
145
+ base = "#{app_root}/app/models/"
146
+ Dir["#{base}**/*.rb"].each do |file|
147
+ model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
225
148
 
226
149
  next if model_name.nil?
227
150
  next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
@@ -229,8 +152,10 @@ index #{model.name.downcase}
229
152
  }
230
153
 
231
154
  begin
232
- model_name.camelize.constantize
233
- rescue NameError, LoadError
155
+ model_name.classify.constantize
156
+ rescue LoadError
157
+ model_name.gsub(/.*[\/\\]/, '').classify.constantize
158
+ rescue NameError
234
159
  next
235
160
  end
236
161
  end
@@ -252,11 +177,68 @@ index #{model.name.downcase}
252
177
  end unless conf.nil?
253
178
  end
254
179
 
180
+ def core_index_for_model(model, sources)
181
+ output = <<-INDEX
182
+
183
+ index #{model.name.downcase}_core
184
+ {
185
+ #{sources}
186
+ path = #{self.searchd_file_path}/#{model.name.downcase}_core
187
+ charset_type = #{self.charset_type}
188
+ INDEX
189
+
190
+ output += " morphology = #{self.morphology}\n" unless self.morphology.blank?
191
+ output += " charset_table = #{self.charset_table}\n" unless self.charset_table.nil?
192
+ output += " ignore_chars = #{self.ignore_chars}\n" unless self.ignore_chars.nil?
193
+
194
+ if self.allow_star
195
+ output += " enable_star = 1\n"
196
+ output += " min_prefix_len = 1\n"
197
+ output += " min_infix_len = 1\n"
198
+ end
199
+
200
+ unless model.indexes.collect(&:prefix_fields).flatten.empty?
201
+ output += " prefix_fields = #{model.indexes.collect(&:prefix_fields).flatten.join(', ')}\n"
202
+ end
203
+
204
+ unless model.indexes.collect(&:infix_fields).flatten.empty?
205
+ output += " infix_fields = #{model.indexes.collect(&:infix_fields).flatten.join(', ')}\n"
206
+ end
207
+
208
+ output + "}\n"
209
+ end
210
+
211
+ def delta_index_for_model(model, sources)
212
+ <<-INDEX
213
+ index #{model.name.downcase}_delta : #{model.name.downcase}_core
214
+ {
215
+ #{sources}
216
+ path = #{self.searchd_file_path}/#{model.name.downcase}_delta
217
+ }
218
+ INDEX
219
+ end
220
+
221
+ def distributed_index_for_model(model)
222
+ sources = ["local = #{model.name.downcase}_core"]
223
+ if model.indexes.any? { |index| index.delta? }
224
+ sources << "local = #{model.name.downcase}_delta"
225
+ end
226
+
227
+ <<-INDEX
228
+ index #{model.name.downcase}
229
+ {
230
+ type = distributed
231
+ #{ sources.join("\n ") }
232
+ charset_type = #{self.charset_type}
233
+ }
234
+ INDEX
235
+ end
236
+
255
237
  def create_array_accum
256
- execute "begin"
257
- execute "savepoint ts"
238
+ ::ActiveRecord::Base.connection.execute "begin"
239
+ ::ActiveRecord::Base.connection.execute "savepoint ts"
258
240
  begin
259
- execute <<-SQL
241
+ ::ActiveRecord::Base.connection.execute <<-SQL
260
242
  CREATE AGGREGATE array_accum (anyelement)
261
243
  (
262
244
  sfunc = array_append,
@@ -266,10 +248,10 @@ index #{model.name.downcase}
266
248
  SQL
267
249
  rescue
268
250
  raise unless $!.to_s =~ /already exists with same argument types/
269
- execute "rollback to savepoint ts"
251
+ ::ActiveRecord::Base.connection.execute "rollback to savepoint ts"
270
252
  end
271
- execute "release savepoint foo"
272
- execute "commit"
253
+ ::ActiveRecord::Base.connection.execute "release savepoint ts"
254
+ ::ActiveRecord::Base.connection.execute "commit"
273
255
  end
274
256
  end
275
257
  end
@@ -8,7 +8,7 @@ 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
11
+ attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes, :prefixes
12
12
 
13
13
  # To create a new field, you'll need to pass in either a single Column
14
14
  # or an array of them, and some (optional) options. The columns are
@@ -17,6 +17,8 @@ module ThinkingSphinx
17
17
  # Valid options are:
18
18
  # - :as => :alias_name
19
19
  # - :sortable => true
20
+ # - :infixes => true
21
+ # - :prefixes => true
20
22
  #
21
23
  # Alias is only required in three circumstances: when there's
22
24
  # another attribute or field with the same name, when the column name is
@@ -27,6 +29,12 @@ module ThinkingSphinx
27
29
  # to an integer value), which can be sorted by. Thinking Sphinx is smart
28
30
  # enough to realise that when you specify fields in sort statements, you
29
31
  # mean their respective attributes.
32
+ #
33
+ # If you have partial matching enabled (ie: enable_star), then you can
34
+ # specify certain fields to have their prefixes and infixes indexed. Keep
35
+ # in mind, though, that Sphinx's default is _all_ fields - so once you
36
+ # highlight a particular field, no other fields in the index will have
37
+ # these partial indexes.
30
38
  #
31
39
  # Here's some examples:
32
40
  #
@@ -41,15 +49,19 @@ module ThinkingSphinx
41
49
  #
42
50
  # Field.new(
43
51
  # [Column.new(:posts, :subject), Column.new(:posts, :content)],
44
- # :as => :posts
52
+ # :as => :posts, :prefixes => true
45
53
  # )
46
54
  #
47
55
  def initialize(columns, options = {})
48
56
  @columns = Array(columns)
49
57
  @associations = {}
58
+
59
+ 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) }
50
60
 
51
61
  @alias = options[:as]
52
62
  @sortable = options[:sortable] || false
63
+ @infixes = options[:infixes] || false
64
+ @prefixes = options[:prefixes] || false
53
65
  end
54
66
 
55
67
  # Get the part of the SELECT clause related to this field. Don't forget
@@ -105,7 +117,7 @@ module ThinkingSphinx
105
117
  when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
106
118
  "CONCAT_WS(' ', #{clause})"
107
119
  when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
108
- clause.split(', ').join(" || #{separator} || ")
120
+ clause.split(', ').join(" || ' ' || ")
109
121
  else
110
122
  clause
111
123
  end
@@ -116,7 +128,7 @@ module ThinkingSphinx
116
128
  when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
117
129
  "GROUP_CONCAT(#{clause} SEPARATOR ' ')"
118
130
  when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
119
- "array_to_string(array_accum(#{clause}), '#{separator}')"
131
+ "array_to_string(array_accum(#{clause}), ' ')"
120
132
  else
121
133
  clause
122
134
  end
@@ -183,4 +195,4 @@ module ThinkingSphinx
183
195
  associations.values.flatten.any? { |assoc| assoc.is_many? }
184
196
  end
185
197
  end
186
- end
198
+ end