thinking-sphinx 1.4.6 → 1.4.7

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