thinking-sphinx 1.4.6 → 1.4.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 (39) hide show
  1. data/README.textile +6 -1
  2. data/features/searching_by_model.feature +24 -30
  3. data/features/thinking_sphinx/db/.gitignore +1 -0
  4. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +1 -0
  5. data/lib/cucumber/thinking_sphinx/internal_world.rb +26 -26
  6. data/lib/thinking_sphinx.rb +17 -26
  7. data/lib/thinking_sphinx/active_record.rb +69 -74
  8. data/lib/thinking_sphinx/active_record/attribute_updates.rb +11 -10
  9. data/lib/thinking_sphinx/active_record/has_many_association.rb +2 -1
  10. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +11 -11
  11. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +34 -20
  12. data/lib/thinking_sphinx/association.rb +12 -7
  13. data/lib/thinking_sphinx/attribute.rb +64 -61
  14. data/lib/thinking_sphinx/configuration.rb +32 -36
  15. data/lib/thinking_sphinx/context.rb +3 -2
  16. data/lib/thinking_sphinx/deploy/capistrano.rb +7 -9
  17. data/lib/thinking_sphinx/search.rb +201 -178
  18. data/lib/thinking_sphinx/source/sql.rb +1 -1
  19. data/lib/thinking_sphinx/tasks.rb +21 -19
  20. data/lib/thinking_sphinx/version.rb +3 -0
  21. data/spec/fixtures/data.sql +32 -0
  22. data/spec/fixtures/database.yml.default +3 -0
  23. data/spec/fixtures/models.rb +161 -0
  24. data/spec/fixtures/structure.sql +146 -0
  25. data/spec/spec_helper.rb +57 -0
  26. data/spec/sphinx_helper.rb +61 -0
  27. data/spec/thinking_sphinx/active_record/delta_spec.rb +24 -24
  28. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +22 -0
  29. data/spec/thinking_sphinx/active_record/scopes_spec.rb +25 -25
  30. data/spec/thinking_sphinx/active_record_spec.rb +110 -109
  31. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +38 -38
  32. data/spec/thinking_sphinx/association_spec.rb +20 -2
  33. data/spec/thinking_sphinx/context_spec.rb +61 -64
  34. data/spec/thinking_sphinx/search_spec.rb +7 -0
  35. data/spec/thinking_sphinx_spec.rb +47 -46
  36. metadata +50 -98
  37. data/VERSION +0 -1
  38. data/tasks/distribution.rb +0 -34
  39. data/tasks/testing.rb +0 -80
@@ -3,12 +3,12 @@ module ThinkingSphinx
3
3
  def initialize(model)
4
4
  @model = model
5
5
  end
6
-
6
+
7
7
  def setup
8
8
  # Deliberately blank - subclasses should do something though. Well, if
9
9
  # they need to.
10
10
  end
11
-
11
+
12
12
  def self.detect(model)
13
13
  adapter = adapter_for_model model
14
14
  case adapter
@@ -22,7 +22,7 @@ module ThinkingSphinx
22
22
  raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{adapter}"
23
23
  end
24
24
  end
25
-
25
+
26
26
  def self.adapter_for_model(model)
27
27
  case ThinkingSphinx.database_adapter
28
28
  when String
@@ -35,7 +35,7 @@ module ThinkingSphinx
35
35
  ThinkingSphinx.database_adapter
36
36
  end
37
37
  end
38
-
38
+
39
39
  def self.standard_adapter_for_model(model)
40
40
  case model.connection.class.name
41
41
  when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
@@ -52,34 +52,34 @@ module ThinkingSphinx
52
52
  when "jdbcpostgresql"
53
53
  :postgresql
54
54
  else
55
- model.connection.config[:adapter]
55
+ model.connection.config[:adapter].to_sym
56
56
  end
57
57
  else
58
58
  model.connection.class.name
59
59
  end
60
60
  end
61
-
61
+
62
62
  def quote_with_table(column)
63
63
  "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
64
64
  end
65
-
65
+
66
66
  def bigint_pattern
67
67
  /bigint/i
68
68
  end
69
-
69
+
70
70
  def downcase(clause)
71
71
  "LOWER(#{clause})"
72
72
  end
73
-
73
+
74
74
  def case(expression, pairs, default)
75
75
  "CASE #{expression} " +
76
76
  pairs.keys.inject('') { |string, key|
77
77
  string + "WHEN '#{key}' THEN #{pairs[key]} "
78
78
  } + "ELSE #{default} END"
79
79
  end
80
-
80
+
81
81
  protected
82
-
82
+
83
83
  def connection
84
84
  @connection ||= @model.connection
85
85
  end
@@ -4,11 +4,11 @@ module ThinkingSphinx
4
4
  create_array_accum_function
5
5
  create_crc32_function
6
6
  end
7
-
7
+
8
8
  def sphinx_identifier
9
9
  "pgsql"
10
10
  end
11
-
11
+
12
12
  def concatenate(clause, separator = ' ')
13
13
  if clause[/^COALESCE/]
14
14
  clause.split('), ').join(") || '#{separator}' || ")
@@ -18,27 +18,31 @@ module ThinkingSphinx
18
18
  }.join(" || '#{separator}' || ")
19
19
  end
20
20
  end
21
-
21
+
22
22
  def group_concatenate(clause, separator = ' ')
23
23
  "array_to_string(array_accum(COALESCE(#{clause}, '0')), '#{separator}')"
24
24
  end
25
-
25
+
26
26
  def cast_to_string(clause)
27
27
  clause
28
28
  end
29
-
29
+
30
30
  def cast_to_datetime(clause)
31
- "cast(extract(epoch from #{clause}) as int)"
31
+ if ThinkingSphinx::Configuration.instance.use_64_bit
32
+ "cast(extract(epoch from #{clause}) as bigint)"
33
+ else
34
+ "cast(extract(epoch from #{clause}) as int)"
35
+ end
32
36
  end
33
-
37
+
34
38
  def cast_to_unsigned(clause)
35
39
  clause
36
40
  end
37
-
41
+
38
42
  def cast_to_int(clause)
39
43
  "#{clause}::INT8"
40
44
  end
41
-
45
+
42
46
  def convert_nulls(clause, default = '')
43
47
  default = case default
44
48
  when String
@@ -50,34 +54,44 @@ module ThinkingSphinx
50
54
  else
51
55
  default
52
56
  end
53
-
57
+
54
58
  "COALESCE(#{clause}, #{default})"
55
59
  end
56
-
60
+
57
61
  def boolean(value)
58
62
  value ? 'TRUE' : 'FALSE'
59
63
  end
60
-
64
+
61
65
  def crc(clause, blank_to_null = false)
62
66
  clause = "NULLIF(#{clause},'')" if blank_to_null
63
67
  "crc32(#{clause})"
64
68
  end
65
-
69
+
66
70
  def utf8_query_pre
67
71
  nil
68
72
  end
69
-
73
+
70
74
  def time_difference(diff)
71
75
  "current_timestamp - interval '#{diff} seconds'"
72
76
  end
73
-
77
+
74
78
  def utc_query_pre
75
79
  "SET TIME ZONE 'UTC'"
76
80
  end
77
-
81
+
78
82
  private
79
-
83
+
80
84
  def execute(command, output_error = false)
85
+ if RUBY_PLATFORM == 'java'
86
+ connection.transaction do
87
+ execute_command command, output_error
88
+ end
89
+ else
90
+ execute_command command, output_error
91
+ end
92
+ end
93
+
94
+ def execute_command(command, output_error = false)
81
95
  connection.execute "begin"
82
96
  connection.execute "savepoint ts"
83
97
  begin
@@ -89,7 +103,7 @@ module ThinkingSphinx
89
103
  connection.execute "release savepoint ts"
90
104
  connection.execute "commit"
91
105
  end
92
-
106
+
93
107
  def create_array_accum_function
94
108
  if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200
95
109
  execute <<-SQL
@@ -112,7 +126,7 @@ module ThinkingSphinx
112
126
  SQL
113
127
  end
114
128
  end
115
-
129
+
116
130
  def create_crc32_function
117
131
  execute "CREATE LANGUAGE 'plpgsql';"
118
132
  function = <<-SQL
@@ -127,7 +141,7 @@ module ThinkingSphinx
127
141
  IF COALESCE(word, '') = '' THEN
128
142
  return 0;
129
143
  END IF;
130
-
144
+
131
145
  i = 0;
132
146
  tmp = 4294967295;
133
147
  byte_length = bit_length(word) / 8;
@@ -45,13 +45,8 @@ module ThinkingSphinx
45
45
 
46
46
  # association is polymorphic - create associations for each
47
47
  # non-polymorphic reflection.
48
- polymorphic_classes(ref).collect { |klass|
49
- Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
50
- ref.macro,
51
- "#{ref.name}_#{klass.name}".to_sym,
52
- casted_options(klass, ref),
53
- ref.active_record
54
- )
48
+ polymorphic_classes(ref).collect { |poly_class|
49
+ Association.new parent, depolymorphic_reflection(ref, klass, poly_class)
55
50
  }
56
51
  end
57
52
 
@@ -119,6 +114,16 @@ module ThinkingSphinx
119
114
 
120
115
  private
121
116
 
117
+ def self.depolymorphic_reflection(reflection, source_class, poly_class)
118
+ name = "#{reflection.name}_#{poly_class.name}".to_sym
119
+
120
+ source_class.reflect_on_association(name) ||
121
+ ::ActiveRecord::Reflection::AssociationReflection.new(
122
+ reflection.macro, name, casted_options(poly_class, reflection),
123
+ reflection.active_record
124
+ )
125
+ end
126
+
122
127
  # Returns all the objects that could be currently instantiated from a
123
128
  # polymorphic association. This is pretty damn fast if there's an index on
124
129
  # the foreign type column - but if there isn't, it can take a while if you
@@ -7,10 +7,10 @@ module ThinkingSphinx
7
7
  # One key thing to remember - if you're using the attribute manually to
8
8
  # generate SQL statements, you'll need to set the base model, and all the
9
9
  # associations. Which can get messy. Use Index.link!, it really helps.
10
- #
10
+ #
11
11
  class Attribute < ThinkingSphinx::Property
12
12
  attr_accessor :query_source
13
-
13
+
14
14
  SphinxTypeMappings = {
15
15
  :multi => :sql_attr_multi,
16
16
  :datetime => :sql_attr_timestamp,
@@ -21,11 +21,11 @@ module ThinkingSphinx
21
21
  :bigint => :sql_attr_bigint,
22
22
  :wordcount => :sql_attr_str2wordcount
23
23
  }
24
-
24
+
25
25
  if Riddle.loaded_version.to_i > 1
26
26
  SphinxTypeMappings[:string] = :sql_attr_string
27
27
  end
28
-
28
+
29
29
  # To create a new attribute, you'll need to pass in either a single Column
30
30
  # or an array of them, and some (optional) options.
31
31
  #
@@ -37,13 +37,13 @@ module ThinkingSphinx
37
37
  # Alias is only required in three circumstances: when there's
38
38
  # another attribute or field with the same name, when the column name is
39
39
  # 'id', or when there's more than one column.
40
- #
40
+ #
41
41
  # Type is not required, unless you want to force a column to be a certain
42
42
  # type (but keep in mind the value will not be CASTed in the SQL
43
43
  # statements). The only time you really need to use this is when the type
44
44
  # can't be figured out by the column - ie: when not actually using a
45
45
  # database column as your source.
46
- #
46
+ #
47
47
  # Source is only used for multi-value attributes (MVA). By default this will
48
48
  # use a left-join and a group_concat to obtain the values. For better performance
49
49
  # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
@@ -81,33 +81,34 @@ module ThinkingSphinx
81
81
  #
82
82
  # If you're creating attributes for latitude and longitude, don't forget
83
83
  # that Sphinx expects these values to be in radians.
84
- #
84
+ #
85
85
  def initialize(source, columns, options = {})
86
86
  super
87
-
87
+
88
88
  @type = options[:type]
89
89
  @query_source = options[:source]
90
90
  @crc = options[:crc]
91
-
91
+ @all_ints = options[:all_ints]
92
+
92
93
  @type ||= :multi unless @query_source.nil?
93
94
  if @type == :string && @crc
94
95
  @type = is_many? ? :multi : :integer
95
96
  end
96
-
97
+
97
98
  source.attributes << self
98
99
  end
99
-
100
+
100
101
  # Get the part of the SELECT clause related to this attribute. Don't forget
101
102
  # to set your model and associations first though.
102
103
  #
103
104
  # This will concatenate strings and arrays of integers, and convert
104
105
  # datetimes to timestamps, as needed.
105
- #
106
+ #
106
107
  def to_select_sql
107
108
  return nil unless include_as_association? && available?
108
-
109
+
109
110
  separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
110
-
111
+
111
112
  clause = columns_with_prefixes.collect { |column|
112
113
  case type
113
114
  when :string
@@ -122,28 +123,28 @@ module ThinkingSphinx
122
123
  column
123
124
  end
124
125
  }.join(', ')
125
-
126
+
126
127
  clause = adapter.crc(clause) if @crc
127
128
  clause = adapter.concatenate(clause, separator) if concat_ws?
128
129
  clause = adapter.group_concatenate(clause, separator) if is_many?
129
130
  clause = adapter.downcase(clause) if insensitive?
130
-
131
+
131
132
  "#{clause} AS #{quote_column(unique_name)}"
132
133
  end
133
-
134
+
134
135
  def type_to_config
135
136
  SphinxTypeMappings[type]
136
137
  end
137
-
138
+
138
139
  def include_as_association?
139
140
  ! (type == :multi && (query_source == :query || query_source == :ranged_query))
140
141
  end
141
-
142
+
142
143
  # Returns the configuration value that should be used for
143
144
  # the attribute.
144
145
  # Special case is the multi-valued attribute that needs some
145
- # extra configuration.
146
- #
146
+ # extra configuration.
147
+ #
147
148
  def config_value(offset = nil, delta = false)
148
149
  if type == :multi
149
150
  multi_config = include_as_association? ? "field" :
@@ -153,12 +154,12 @@ module ThinkingSphinx
153
154
  unique_name
154
155
  end
155
156
  end
156
-
157
+
157
158
  # Returns the type of the column. If that's not already set, it returns
158
159
  # :multi if there's the possibility of more than one value, :string if
159
160
  # there's more than one association, otherwise it figures out what the
160
161
  # actual column's datatype is and returns that.
161
- #
162
+ #
162
163
  def type
163
164
  @type ||= begin
164
165
  base_type = case
@@ -169,21 +170,23 @@ module ThinkingSphinx
169
170
  else
170
171
  translated_type_from_database
171
172
  end
172
-
173
+
173
174
  if base_type == :string && @crc
174
175
  base_type = :integer
175
176
  else
176
177
  @crc = false unless base_type == :multi && is_many_strings? && @crc
177
178
  end
178
-
179
+
179
180
  base_type
180
181
  end
181
182
  end
182
-
183
+
183
184
  def updatable?
184
- [:integer, :datetime, :boolean].include?(type) && !is_string?
185
+ [:integer, :datetime, :boolean].include?(type) &&
186
+ unique_name != :sphinx_internal_id &&
187
+ !is_string?
185
188
  end
186
-
189
+
187
190
  def live_value(instance)
188
191
  object = instance
189
192
  column = @columns.first
@@ -191,29 +194,29 @@ module ThinkingSphinx
191
194
  object = object.send(method)
192
195
  return sphinx_value(nil) if object.nil?
193
196
  }
194
-
197
+
195
198
  sphinx_value object.send(column.__name)
196
199
  end
197
-
200
+
198
201
  def all_ints?
199
- all_of_type?(:integer)
202
+ @all_ints || all_of_type?(:integer)
200
203
  end
201
-
204
+
202
205
  def all_datetimes?
203
206
  all_of_type?(:datetime, :date, :timestamp)
204
207
  end
205
-
208
+
206
209
  def all_strings?
207
210
  all_of_type?(:string, :text)
208
211
  end
209
-
212
+
210
213
  private
211
-
214
+
212
215
  def source_value(offset, delta)
213
216
  if is_string?
214
217
  return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
215
218
  end
216
-
219
+
217
220
  query = query(offset)
218
221
 
219
222
  if query_source == :ranged_query
@@ -225,12 +228,12 @@ module ThinkingSphinx
225
228
  "query; #{query}"
226
229
  end
227
230
  end
228
-
231
+
229
232
  def query(offset)
230
233
  base_assoc = base_association_for_mva
231
234
  end_assoc = end_association_for_mva
232
235
  raise "Could not determine SQL for MVA" if base_assoc.nil?
233
-
236
+
234
237
  <<-SQL
235
238
  SELECT #{foreign_key_for_mva base_assoc}
236
239
  #{ThinkingSphinx.unique_id_expression(adapter, offset)} AS #{quote_column('id')},
@@ -238,12 +241,12 @@ SELECT #{foreign_key_for_mva base_assoc}
238
241
  FROM #{quote_table_name base_assoc.table} #{association_joins}
239
242
  SQL
240
243
  end
241
-
244
+
242
245
  def query_clause
243
246
  foreign_key = foreign_key_for_mva base_association_for_mva
244
247
  "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
245
248
  end
246
-
249
+
247
250
  def query_delta
248
251
  foreign_key = foreign_key_for_mva base_association_for_mva
249
252
  <<-SQL
@@ -252,40 +255,40 @@ FROM #{model.quoted_table_name}
252
255
  WHERE #{@source.index.delta_object.clause(model, true)})
253
256
  SQL
254
257
  end
255
-
258
+
256
259
  def range_query
257
260
  assoc = base_association_for_mva
258
261
  foreign_key = foreign_key_for_mva assoc
259
262
  "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
260
263
  end
261
-
264
+
262
265
  def primary_key_for_mva(assoc)
263
266
  quote_with_table(
264
267
  assoc.table, assoc.primary_key_from_reflection || columns.first.__name
265
268
  )
266
269
  end
267
-
270
+
268
271
  def foreign_key_for_mva(assoc)
269
272
  quote_with_table assoc.table, assoc.reflection.primary_key_name
270
273
  end
271
-
274
+
272
275
  def end_association_for_mva
273
276
  @association_for_mva ||= associations[columns.first].detect { |assoc|
274
277
  assoc.has_column?(columns.first.__name)
275
278
  }
276
279
  end
277
-
280
+
278
281
  def base_association_for_mva
279
282
  @first_association_for_mva ||= begin
280
283
  assoc = end_association_for_mva
281
284
  while !assoc.parent.nil?
282
285
  assoc = assoc.parent
283
286
  end
284
-
287
+
285
288
  assoc
286
289
  end
287
290
  end
288
-
291
+
289
292
  def association_joins
290
293
  joins = []
291
294
  assoc = end_association_for_mva
@@ -293,22 +296,22 @@ WHERE #{@source.index.delta_object.clause(model, true)})
293
296
  joins << assoc.to_sql
294
297
  assoc = assoc.parent
295
298
  end
296
-
299
+
297
300
  joins.join(' ')
298
301
  end
299
-
302
+
300
303
  def is_many_ints?
301
304
  concat_ws? && all_ints?
302
305
  end
303
-
306
+
304
307
  def is_many_datetimes?
305
308
  is_many? && all_datetimes?
306
309
  end
307
-
310
+
308
311
  def is_many_strings?
309
312
  is_many? && all_strings?
310
313
  end
311
-
314
+
312
315
  def translated_type_from_database
313
316
  case type_from_db = type_from_database
314
317
  when :integer
@@ -330,16 +333,16 @@ block:
330
333
  MESSAGE
331
334
  end
332
335
  end
333
-
336
+
334
337
  def type_from_database
335
338
  column = column_from_db
336
339
  column.nil? ? nil : column.type
337
340
  end
338
-
341
+
339
342
  def integer_type_from_db
340
343
  column = column_from_db
341
344
  return nil if column.nil?
342
-
345
+
343
346
  case column.sql_type
344
347
  when adapter.bigint_pattern
345
348
  :bigint
@@ -347,16 +350,16 @@ block:
347
350
  :integer
348
351
  end
349
352
  end
350
-
353
+
351
354
  def column_from_db
352
- klass = @associations.values.flatten.first ?
355
+ klass = @associations.values.flatten.first ?
353
356
  @associations.values.flatten.first.reflection.klass : @model
354
-
357
+
355
358
  klass.columns.detect { |col|
356
359
  @columns.collect { |c| c.__name.to_s }.include? col.name
357
360
  }
358
361
  end
359
-
362
+
360
363
  def all_of_type?(*column_types)
361
364
  @columns.all? { |col|
362
365
  klasses = @associations[col].empty? ? [@model] :
@@ -367,7 +370,7 @@ block:
367
370
  }
368
371
  }
369
372
  end
370
-
373
+
371
374
  def sphinx_value(value)
372
375
  case value
373
376
  when TrueClass
@@ -384,7 +387,7 @@ block:
384
387
  value
385
388
  end
386
389
  end
387
-
390
+
388
391
  def insensitive?
389
392
  @sortable == :insensitive
390
393
  end