thinking-sphinx 2.0.6 → 2.0.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.
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