thinking-sphinx 2.0.6 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/HISTORY +157 -0
  2. data/lib/cucumber/thinking_sphinx/external_world.rb +12 -0
  3. data/lib/cucumber/thinking_sphinx/internal_world.rb +127 -0
  4. data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
  5. data/lib/thinking-sphinx.rb +1 -0
  6. data/lib/thinking_sphinx/action_controller.rb +31 -0
  7. data/lib/thinking_sphinx/active_record/attribute_updates.rb +53 -0
  8. data/lib/thinking_sphinx/active_record/collection_proxy.rb +40 -0
  9. data/lib/thinking_sphinx/active_record/collection_proxy_with_scopes.rb +27 -0
  10. data/lib/thinking_sphinx/active_record/delta.rb +65 -0
  11. data/lib/thinking_sphinx/active_record/has_many_association.rb +37 -0
  12. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +21 -0
  13. data/lib/thinking_sphinx/active_record/log_subscriber.rb +61 -0
  14. data/lib/thinking_sphinx/active_record/scopes.rb +110 -0
  15. data/lib/thinking_sphinx/active_record.rb +383 -0
  16. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +87 -0
  17. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +62 -0
  18. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +171 -0
  19. data/lib/thinking_sphinx/association.rb +229 -0
  20. data/lib/thinking_sphinx/attribute.rb +407 -0
  21. data/lib/thinking_sphinx/auto_version.rb +38 -0
  22. data/lib/thinking_sphinx/bundled_search.rb +44 -0
  23. data/lib/thinking_sphinx/class_facet.rb +20 -0
  24. data/lib/thinking_sphinx/configuration.rb +335 -0
  25. data/lib/thinking_sphinx/context.rb +77 -0
  26. data/lib/thinking_sphinx/core/string.rb +15 -0
  27. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  28. data/lib/thinking_sphinx/deltas.rb +28 -0
  29. data/lib/thinking_sphinx/deploy/capistrano.rb +99 -0
  30. data/lib/thinking_sphinx/excerpter.rb +23 -0
  31. data/lib/thinking_sphinx/facet.rb +128 -0
  32. data/lib/thinking_sphinx/facet_search.rb +170 -0
  33. data/lib/thinking_sphinx/field.rb +98 -0
  34. data/lib/thinking_sphinx/index/builder.rb +312 -0
  35. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  36. data/lib/thinking_sphinx/index.rb +157 -0
  37. data/lib/thinking_sphinx/join.rb +37 -0
  38. data/lib/thinking_sphinx/property.rb +185 -0
  39. data/lib/thinking_sphinx/railtie.rb +46 -0
  40. data/lib/thinking_sphinx/search.rb +995 -0
  41. data/lib/thinking_sphinx/search_methods.rb +439 -0
  42. data/lib/thinking_sphinx/sinatra.rb +7 -0
  43. data/lib/thinking_sphinx/source/internal_properties.rb +51 -0
  44. data/lib/thinking_sphinx/source/sql.rb +157 -0
  45. data/lib/thinking_sphinx/source.rb +194 -0
  46. data/lib/thinking_sphinx/tasks.rb +132 -0
  47. data/lib/thinking_sphinx/test.rb +55 -0
  48. data/lib/thinking_sphinx/version.rb +3 -0
  49. data/lib/thinking_sphinx.rb +296 -0
  50. metadata +53 -4
@@ -0,0 +1,407 @@
1
+ module ThinkingSphinx
2
+ # Attributes - eternally useful when it comes to filtering, sorting or
3
+ # grouping. This class isn't really useful to you unless you're hacking
4
+ # around with the internals of Thinking Sphinx - but hey, don't let that
5
+ # stop you.
6
+ #
7
+ # One key thing to remember - if you're using the attribute manually to
8
+ # generate SQL statements, you'll need to set the base model, and all the
9
+ # associations. Which can get messy. Use Index.link!, it really helps.
10
+ #
11
+ class Attribute < ThinkingSphinx::Property
12
+ attr_accessor :query_source
13
+
14
+ SphinxTypeMappings = {
15
+ :multi => :sql_attr_multi,
16
+ :datetime => :sql_attr_timestamp,
17
+ :string => :sql_attr_str2ordinal,
18
+ :float => :sql_attr_float,
19
+ :boolean => :sql_attr_bool,
20
+ :integer => :sql_attr_uint,
21
+ :bigint => :sql_attr_bigint,
22
+ :wordcount => :sql_attr_str2wordcount
23
+ }
24
+
25
+ if Riddle.loaded_version.to_i > 1
26
+ SphinxTypeMappings[:string] = :sql_attr_string
27
+ end
28
+
29
+ # To create a new attribute, you'll need to pass in either a single Column
30
+ # or an array of them, and some (optional) options.
31
+ #
32
+ # Valid options are:
33
+ # - :as => :alias_name
34
+ # - :type => :attribute_type
35
+ # - :source => :field, :query, :ranged_query
36
+ #
37
+ # Alias is only required in three circumstances: when there's
38
+ # another attribute or field with the same name, when the column name is
39
+ # 'id', or when there's more than one column.
40
+ #
41
+ # Type is not required, unless you want to force a column to be a certain
42
+ # type (but keep in mind the value will not be CASTed in the SQL
43
+ # statements). The only time you really need to use this is when the type
44
+ # can't be figured out by the column - ie: when not actually using a
45
+ # database column as your source.
46
+ #
47
+ # Source is only used for multi-value attributes (MVA). By default this will
48
+ # use a left-join and a group_concat to obtain the values. For better performance
49
+ # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
50
+ # all document,value-pairs.
51
+ # Either :query or :ranged_query will enable this feature, where :ranged_query will cause
52
+ # the query to be executed incremental.
53
+ #
54
+ # Example usage:
55
+ #
56
+ # Attribute.new(
57
+ # Column.new(:created_at)
58
+ # )
59
+ #
60
+ # Attribute.new(
61
+ # Column.new(:posts, :id),
62
+ # :as => :post_ids
63
+ # )
64
+ #
65
+ # Attribute.new(
66
+ # Column.new(:posts, :id),
67
+ # :as => :post_ids,
68
+ # :source => :ranged_query
69
+ # )
70
+ #
71
+ # Attribute.new(
72
+ # [Column.new(:pages, :id), Column.new(:articles, :id)],
73
+ # :as => :content_ids
74
+ # )
75
+ #
76
+ # Attribute.new(
77
+ # Column.new("NOW()"),
78
+ # :as => :indexed_at,
79
+ # :type => :datetime
80
+ # )
81
+ #
82
+ # If you're creating attributes for latitude and longitude, don't forget
83
+ # that Sphinx expects these values to be in radians.
84
+ #
85
+ def initialize(source, columns, options = {})
86
+ super
87
+
88
+ @type = options[:type]
89
+ @query_source = options[:source]
90
+ @crc = options[:crc]
91
+ @all_ints = options[:all_ints]
92
+
93
+ @type ||= :multi unless @query_source.nil?
94
+ if @type == :string && @crc
95
+ @type = is_many? ? :multi : :integer
96
+ end
97
+
98
+ source.attributes << self
99
+ end
100
+
101
+ # Get the part of the SELECT clause related to this attribute. Don't forget
102
+ # to set your model and associations first though.
103
+ #
104
+ # This will concatenate strings and arrays of integers, and convert
105
+ # datetimes to timestamps, as needed.
106
+ #
107
+ def to_select_sql
108
+ return nil unless include_as_association? && available?
109
+
110
+ separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
111
+
112
+ clause = columns_with_prefixes.collect { |column|
113
+ case type
114
+ when :string
115
+ adapter.convert_nulls(column)
116
+ when :datetime
117
+ adapter.cast_to_datetime(column)
118
+ when :multi
119
+ column = adapter.cast_to_datetime(column) if is_many_datetimes?
120
+ column = adapter.convert_nulls(column, '0') if is_many_ints?
121
+ column
122
+ else
123
+ column
124
+ end
125
+ }.join(', ')
126
+
127
+ clause = adapter.crc(clause) if @crc
128
+ clause = adapter.concatenate(clause, separator) if concat_ws?
129
+ clause = adapter.group_concatenate(clause, separator) if is_many?
130
+ clause = adapter.downcase(clause) if insensitive?
131
+
132
+ "#{clause} AS #{quote_column(unique_name)}"
133
+ end
134
+
135
+ def type_to_config
136
+ SphinxTypeMappings[type]
137
+ end
138
+
139
+ def include_as_association?
140
+ ! (type == :multi && (query_source == :query || query_source == :ranged_query))
141
+ end
142
+
143
+ # Returns the configuration value that should be used for
144
+ # the attribute.
145
+ # Special case is the multi-valued attribute that needs some
146
+ # extra configuration.
147
+ #
148
+ def config_value(offset = nil, delta = false)
149
+ if type == :multi
150
+ multi_config = include_as_association? ? "field" :
151
+ source_value(offset, delta).gsub(/\s+/m, " ").strip
152
+ "uint #{unique_name} from #{multi_config}"
153
+ else
154
+ unique_name
155
+ end
156
+ end
157
+
158
+ # Returns the type of the column. If that's not already set, it returns
159
+ # :multi if there's the possibility of more than one value, :string if
160
+ # there's more than one association, otherwise it figures out what the
161
+ # actual column's datatype is and returns that.
162
+ #
163
+ def type
164
+ @type ||= begin
165
+ base_type = case
166
+ when is_many?, is_many_ints?
167
+ :multi
168
+ when @associations.values.flatten.length > 1
169
+ :string
170
+ else
171
+ translated_type_from_database
172
+ end
173
+
174
+ if base_type == :string && @crc
175
+ base_type = :integer
176
+ else
177
+ @crc = false unless base_type == :multi && is_many_strings? && @crc
178
+ end
179
+
180
+ base_type
181
+ end
182
+ end
183
+
184
+ def updatable?
185
+ [:integer, :datetime, :boolean].include?(type) &&
186
+ unique_name != :sphinx_internal_id &&
187
+ !is_string?
188
+ end
189
+
190
+ def live_value(instance)
191
+ object = instance
192
+ column = @columns.first
193
+ column.__stack.each { |method|
194
+ object = object.send(method)
195
+ return sphinx_value(nil) if object.nil?
196
+ }
197
+
198
+ sphinx_value object.send(column.__name)
199
+ end
200
+
201
+ def all_ints?
202
+ @all_ints || all_of_type?(:integer)
203
+ end
204
+
205
+ def all_datetimes?
206
+ all_of_type?(:datetime, :date, :timestamp)
207
+ end
208
+
209
+ def all_strings?
210
+ all_of_type?(:string, :text)
211
+ end
212
+
213
+ private
214
+
215
+ def source_value(offset, delta)
216
+ if is_string?
217
+ return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
218
+ end
219
+
220
+ query = query(offset)
221
+
222
+ if query_source == :ranged_query
223
+ query += query_clause
224
+ query += " AND #{query_delta.strip}" if delta
225
+ "ranged-query; #{query}; #{range_query}"
226
+ else
227
+ query += " WHERE #{query_delta.strip}" if delta
228
+ "query; #{query}"
229
+ end
230
+ end
231
+
232
+ def query(offset)
233
+ base_assoc = base_association_for_mva
234
+ end_assoc = end_association_for_mva
235
+ raise "Could not determine SQL for MVA" if base_assoc.nil?
236
+
237
+ relation = Arel::Table.new(base_assoc.table)
238
+
239
+ association_joins.each do |join|
240
+ relation = relation.join(join.relation, Arel::OuterJoin).
241
+ on(*join.association_join)
242
+ end
243
+
244
+ relation = relation.project "#{foreign_key_for_mva base_assoc} #{ThinkingSphinx.unique_id_expression(adapter, offset)} AS #{quote_column('id')}, #{primary_key_for_mva(end_assoc)} AS #{quote_column(unique_name)}"
245
+
246
+ relation.to_sql
247
+ end
248
+
249
+ def query_clause
250
+ foreign_key = foreign_key_for_mva base_association_for_mva
251
+ " WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
252
+ end
253
+
254
+ def query_delta
255
+ foreign_key = foreign_key_for_mva base_association_for_mva
256
+ <<-SQL
257
+ #{foreign_key} IN (SELECT #{quote_column model.primary_key}
258
+ FROM #{model.quoted_table_name}
259
+ WHERE #{@source.index.delta_object.clause(model, true)})
260
+ SQL
261
+ end
262
+
263
+ def range_query
264
+ assoc = base_association_for_mva
265
+ foreign_key = foreign_key_for_mva assoc
266
+ "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
267
+ end
268
+
269
+ def primary_key_for_mva(assoc)
270
+ quote_with_table(
271
+ assoc.table, assoc.primary_key_from_reflection || columns.first.__name
272
+ )
273
+ end
274
+
275
+ def foreign_key_for_mva(assoc)
276
+ if ThinkingSphinx.rails_3_1?
277
+ if assoc.reflection.through_reflection
278
+ quote_with_table assoc.table, assoc.reflection.through_reflection.foreign_key
279
+ else
280
+ quote_with_table assoc.table, assoc.reflection.foreign_key
281
+ end
282
+ else
283
+ quote_with_table assoc.table, assoc.reflection.primary_key_name
284
+ end
285
+ end
286
+
287
+ def end_association_for_mva
288
+ @association_for_mva ||= associations[columns.first].detect { |assoc|
289
+ assoc.has_column?(columns.first.__name)
290
+ }
291
+ end
292
+
293
+ def base_association_for_mva
294
+ @first_association_for_mva ||= begin
295
+ assoc = end_association_for_mva
296
+ while !assoc.parent.nil?
297
+ assoc = assoc.parent
298
+ end
299
+
300
+ assoc
301
+ end
302
+ end
303
+
304
+ def association_joins
305
+ joins = []
306
+ assoc = end_association_for_mva
307
+ while assoc != base_association_for_mva
308
+ joins << assoc.join
309
+ assoc = assoc.parent
310
+ end
311
+
312
+ joins
313
+ end
314
+
315
+ def is_many_ints?
316
+ concat_ws? && all_ints?
317
+ end
318
+
319
+ def is_many_datetimes?
320
+ is_many? && all_datetimes?
321
+ end
322
+
323
+ def is_many_strings?
324
+ is_many? && all_strings?
325
+ end
326
+
327
+ def translated_type_from_database
328
+ case type_from_db = type_from_database
329
+ when :integer
330
+ integer_type_from_db
331
+ when :datetime, :string, :float, :boolean
332
+ type_from_db
333
+ when :decimal
334
+ :float
335
+ when :timestamp, :date
336
+ :datetime
337
+ else
338
+ raise <<-MESSAGE
339
+
340
+ Cannot automatically map attribute #{unique_name} in #{@model.name} to an
341
+ equivalent Sphinx type (integer, float, boolean, datetime, string as ordinal).
342
+ You could try to explicitly convert the column's value in your define_index
343
+ block:
344
+ has "CAST(column AS INT)", :type => :integer, :as => :column
345
+ MESSAGE
346
+ end
347
+ end
348
+
349
+ def type_from_database
350
+ column = column_from_db
351
+ column.nil? ? nil : column.type
352
+ end
353
+
354
+ def integer_type_from_db
355
+ column = column_from_db
356
+ return nil if column.nil?
357
+
358
+ case column.sql_type
359
+ when adapter.bigint_pattern
360
+ :bigint
361
+ else
362
+ :integer
363
+ end
364
+ end
365
+
366
+ def column_from_db
367
+ klass = @associations.values.flatten.first ?
368
+ @associations.values.flatten.first.reflection.klass : @model
369
+
370
+ klass.columns.detect { |col|
371
+ @columns.collect { |c| c.__name.to_s }.include? col.name
372
+ }
373
+ end
374
+
375
+ def all_of_type?(*column_types)
376
+ @columns.all? { |col|
377
+ klasses = @associations[col].empty? ? [@model] :
378
+ @associations[col].collect { |assoc| assoc.reflection.klass }
379
+ klasses.all? { |klass|
380
+ column = klass.columns.detect { |column| column.name == col.__name.to_s }
381
+ !column.nil? && column_types.include?(column.type)
382
+ }
383
+ }
384
+ end
385
+
386
+ def sphinx_value(value)
387
+ case value
388
+ when TrueClass
389
+ 1
390
+ when FalseClass, NilClass
391
+ 0
392
+ when Time
393
+ value.to_i
394
+ when Date
395
+ value.to_time.to_i
396
+ when String
397
+ value.to_crc32
398
+ else
399
+ value
400
+ end
401
+ end
402
+
403
+ def insensitive?
404
+ @sortable == :insensitive
405
+ end
406
+ end
407
+ end
@@ -0,0 +1,38 @@
1
+ module ThinkingSphinx
2
+ class AutoVersion
3
+ def self.detect
4
+ version = ThinkingSphinx::Configuration.instance.version
5
+ case version
6
+ when '0.9.8', '0.9.9'
7
+ require "riddle/#{version}"
8
+ when /1.10/
9
+ require 'riddle/1.10'
10
+ when /2.0.\d/
11
+ require 'riddle/2.0.1'
12
+ else
13
+ documentation_link = %Q{
14
+ For more information, read the documentation:
15
+ http://freelancing-god.github.com/ts/en/advanced_config.html
16
+ }
17
+
18
+ if version.nil? || version.empty?
19
+ STDERR.puts %Q{
20
+ Sphinx cannot be found on your system. You may need to configure the following
21
+ settings in your config/sphinx.yml file:
22
+ * bin_path
23
+ * searchd_binary_name
24
+ * indexer_binary_name
25
+
26
+ #{documentation_link}
27
+ }
28
+ else
29
+ STDERR.puts %Q{
30
+ Unsupported version: #{version}
31
+
32
+ #{documentation_link}
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ module ThinkingSphinx
2
+ class BundledSearch
3
+ attr_reader :client
4
+
5
+ def initialize
6
+ @searches = []
7
+ end
8
+
9
+ def search(*args)
10
+ @searches << ThinkingSphinx.search(*args)
11
+ @searches.last.append_to client
12
+ end
13
+
14
+ def search_for_ids(*args)
15
+ @searches << ThinkingSphinx.search_for_ids(*args)
16
+ @searches.last.append_to client
17
+ end
18
+
19
+ def searches
20
+ populate
21
+ @searches
22
+ end
23
+
24
+ private
25
+
26
+ def client
27
+ @client ||= ThinkingSphinx::Configuration.instance.client
28
+ end
29
+
30
+ def populated?
31
+ @populated
32
+ end
33
+
34
+ def populate
35
+ return if populated?
36
+
37
+ @populated = true
38
+
39
+ client.run.each_with_index do |results, index|
40
+ searches[index].populate_from_queue results
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ module ThinkingSphinx
2
+ class ClassFacet < ThinkingSphinx::Facet
3
+ def name
4
+ :class
5
+ end
6
+
7
+ def attribute_name
8
+ Riddle.loaded_version.to_i < 2 ? 'class_crc' : 'sphinx_internal_class'
9
+ end
10
+
11
+ def value(object, attribute_hash)
12
+ if Riddle.loaded_version.to_i < 2
13
+ crc = attribute_hash['class_crc']
14
+ ThinkingSphinx::Configuration.instance.models_by_crc[crc]
15
+ else
16
+ attribute_hash['sphinx_internal_class']
17
+ end
18
+ end
19
+ end
20
+ end