thinking-sphinx 1.5.0 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/README.textile +15 -48
  2. data/VERSION +1 -0
  3. data/features/attribute_transformation.feature +7 -7
  4. data/features/attribute_updates.feature +16 -18
  5. data/features/deleting_instances.feature +13 -16
  6. data/features/excerpts.feature +0 -8
  7. data/features/facets.feature +19 -25
  8. data/features/handling_edits.feature +20 -25
  9. data/features/searching_across_models.feature +1 -1
  10. data/features/searching_by_index.feature +5 -6
  11. data/features/searching_by_model.feature +29 -29
  12. data/features/sphinx_scopes.feature +0 -26
  13. data/features/step_definitions/common_steps.rb +6 -18
  14. data/features/step_definitions/scope_steps.rb +0 -4
  15. data/features/step_definitions/search_steps.rb +4 -9
  16. data/features/support/env.rb +10 -3
  17. data/features/thinking_sphinx/db/fixtures/alphas.rb +10 -8
  18. data/features/thinking_sphinx/db/fixtures/cats.rb +1 -1
  19. data/features/thinking_sphinx/db/fixtures/dogs.rb +1 -1
  20. data/features/thinking_sphinx/db/fixtures/foxes.rb +1 -1
  21. data/features/thinking_sphinx/db/fixtures/people.rb +1 -1
  22. data/features/thinking_sphinx/db/fixtures/posts.rb +1 -5
  23. data/features/thinking_sphinx/db/migrations/create_posts.rb +0 -1
  24. data/features/thinking_sphinx/models/alpha.rb +0 -1
  25. data/features/thinking_sphinx/models/beta.rb +0 -5
  26. data/features/thinking_sphinx/models/developer.rb +1 -6
  27. data/features/thinking_sphinx/models/music.rb +1 -3
  28. data/features/thinking_sphinx/models/person.rb +1 -2
  29. data/features/thinking_sphinx/models/post.rb +0 -1
  30. data/lib/cucumber/thinking_sphinx/external_world.rb +4 -8
  31. data/lib/cucumber/thinking_sphinx/internal_world.rb +27 -36
  32. data/lib/thinking_sphinx.rb +60 -132
  33. data/lib/thinking_sphinx/active_record.rb +98 -124
  34. data/lib/thinking_sphinx/active_record/attribute_updates.rb +13 -17
  35. data/lib/thinking_sphinx/active_record/delta.rb +15 -21
  36. data/lib/thinking_sphinx/active_record/has_many_association.rb +23 -16
  37. data/lib/thinking_sphinx/active_record/scopes.rb +0 -18
  38. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +15 -63
  39. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +0 -4
  40. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +24 -65
  41. data/lib/thinking_sphinx/association.rb +11 -36
  42. data/lib/thinking_sphinx/attribute.rb +85 -92
  43. data/lib/thinking_sphinx/auto_version.rb +3 -21
  44. data/lib/thinking_sphinx/class_facet.rb +3 -8
  45. data/lib/thinking_sphinx/configuration.rb +58 -114
  46. data/lib/thinking_sphinx/context.rb +20 -22
  47. data/lib/thinking_sphinx/core/array.rb +13 -0
  48. data/lib/thinking_sphinx/deltas.rb +0 -2
  49. data/lib/thinking_sphinx/deltas/default_delta.rb +22 -18
  50. data/lib/thinking_sphinx/deploy/capistrano.rb +31 -30
  51. data/lib/thinking_sphinx/excerpter.rb +1 -2
  52. data/lib/thinking_sphinx/facet.rb +35 -45
  53. data/lib/thinking_sphinx/facet_search.rb +24 -58
  54. data/lib/thinking_sphinx/field.rb +0 -18
  55. data/lib/thinking_sphinx/index.rb +36 -38
  56. data/lib/thinking_sphinx/index/builder.rb +59 -74
  57. data/lib/thinking_sphinx/property.rb +45 -66
  58. data/lib/thinking_sphinx/railtie.rb +35 -0
  59. data/lib/thinking_sphinx/search.rb +250 -506
  60. data/lib/thinking_sphinx/source.rb +31 -50
  61. data/lib/thinking_sphinx/source/internal_properties.rb +3 -8
  62. data/lib/thinking_sphinx/source/sql.rb +31 -71
  63. data/lib/thinking_sphinx/tasks.rb +27 -48
  64. data/spec/thinking_sphinx/active_record/delta_spec.rb +41 -36
  65. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +0 -96
  66. data/spec/thinking_sphinx/active_record/scopes_spec.rb +29 -29
  67. data/spec/thinking_sphinx/active_record_spec.rb +169 -140
  68. data/spec/thinking_sphinx/association_spec.rb +2 -20
  69. data/spec/thinking_sphinx/attribute_spec.rb +97 -101
  70. data/spec/thinking_sphinx/auto_version_spec.rb +11 -75
  71. data/spec/thinking_sphinx/configuration_spec.rb +62 -63
  72. data/spec/thinking_sphinx/context_spec.rb +66 -66
  73. data/spec/thinking_sphinx/facet_search_spec.rb +99 -99
  74. data/spec/thinking_sphinx/facet_spec.rb +4 -30
  75. data/spec/thinking_sphinx/field_spec.rb +3 -17
  76. data/spec/thinking_sphinx/index/builder_spec.rb +132 -169
  77. data/spec/thinking_sphinx/index_spec.rb +39 -45
  78. data/spec/thinking_sphinx/search_methods_spec.rb +33 -37
  79. data/spec/thinking_sphinx/search_spec.rb +269 -491
  80. data/spec/thinking_sphinx/source_spec.rb +48 -62
  81. data/spec/thinking_sphinx_spec.rb +49 -49
  82. data/tasks/distribution.rb +46 -0
  83. data/tasks/testing.rb +74 -0
  84. metadata +123 -199
  85. data/features/field_sorting.feature +0 -18
  86. data/features/thinking_sphinx/db/.gitignore +0 -1
  87. data/features/thinking_sphinx/db/fixtures/post_keywords.txt +0 -1
  88. data/features/thinking_sphinx/models/andrew.rb +0 -17
  89. data/lib/thinking-sphinx.rb +0 -1
  90. data/lib/thinking_sphinx/active_record/has_many_association_with_scopes.rb +0 -21
  91. data/lib/thinking_sphinx/bundled_search.rb +0 -40
  92. data/lib/thinking_sphinx/connection.rb +0 -71
  93. data/lib/thinking_sphinx/deltas/delete_job.rb +0 -16
  94. data/lib/thinking_sphinx/deltas/index_job.rb +0 -17
  95. data/lib/thinking_sphinx/rails_additions.rb +0 -181
  96. data/spec/fixtures/data.sql +0 -32
  97. data/spec/fixtures/database.yml.default +0 -3
  98. data/spec/fixtures/models.rb +0 -161
  99. data/spec/fixtures/structure.sql +0 -146
  100. data/spec/spec_helper.rb +0 -54
  101. data/spec/sphinx_helper.rb +0 -67
  102. data/spec/thinking_sphinx/adapters/abstract_adapter_spec.rb +0 -163
  103. data/spec/thinking_sphinx/connection_spec.rb +0 -77
  104. data/spec/thinking_sphinx/rails_additions_spec.rb +0 -203
@@ -45,8 +45,13 @@ module ThinkingSphinx
45
45
 
46
46
  # association is polymorphic - create associations for each
47
47
  # non-polymorphic reflection.
48
- polymorphic_classes(ref).collect { |poly_class|
49
- Association.new parent, depolymorphic_reflection(ref, klass, poly_class)
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
+ )
50
55
  }
51
56
  end
52
57
 
@@ -66,7 +71,9 @@ module ThinkingSphinx
66
71
  # join conditions avoid column name collisions.
67
72
  #
68
73
  def to_sql
69
- rewrite_conditions
74
+ @join.to_sql.gsub(/::ts_join_alias::/,
75
+ "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
76
+ )
70
77
  end
71
78
 
72
79
  # Returns true if the association - or a parent - is a has_many or
@@ -114,16 +121,6 @@ module ThinkingSphinx
114
121
 
115
122
  private
116
123
 
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
-
127
124
  # Returns all the objects that could be currently instantiated from a
128
125
  # polymorphic association. This is pretty damn fast if there's an index on
129
126
  # the foreign type column - but if there isn't, it can take a while if you
@@ -163,27 +160,5 @@ module ThinkingSphinx
163
160
 
164
161
  options
165
162
  end
166
-
167
- def rewrite_conditions
168
- rewrite_condition @join.association_join
169
- end
170
-
171
- def rewrite_condition(condition)
172
- return condition unless condition.is_a?(String)
173
-
174
- if defined?(ActsAsTaggableOn) &&
175
- @reflection.klass == ActsAsTaggableOn::Tagging &&
176
- @reflection.name.to_s[/_taggings$/]
177
- condition = condition.gsub /taggings\./, "#{quoted_alias @join}."
178
- end
179
-
180
- condition.gsub /::ts_join_alias::/, quoted_alias(@join.parent)
181
- end
182
-
183
- def quoted_alias(join)
184
- @reflection.klass.connection.quote_table_name(
185
- join.aliased_table_name
186
- )
187
- end
188
163
  end
189
- end
164
+ end
@@ -7,21 +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
-
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
-
13
+
25
14
  # To create a new attribute, you'll need to pass in either a single Column
26
15
  # or an array of them, and some (optional) options.
27
16
  #
@@ -33,13 +22,13 @@ module ThinkingSphinx
33
22
  # Alias is only required in three circumstances: when there's
34
23
  # another attribute or field with the same name, when the column name is
35
24
  # 'id', or when there's more than one column.
36
- #
25
+ #
37
26
  # Type is not required, unless you want to force a column to be a certain
38
27
  # type (but keep in mind the value will not be CASTed in the SQL
39
28
  # statements). The only time you really need to use this is when the type
40
29
  # can't be figured out by the column - ie: when not actually using a
41
30
  # database column as your source.
42
- #
31
+ #
43
32
  # Source is only used for multi-value attributes (MVA). By default this will
44
33
  # use a left-join and a group_concat to obtain the values. For better performance
45
34
  # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
@@ -77,34 +66,33 @@ module ThinkingSphinx
77
66
  #
78
67
  # If you're creating attributes for latitude and longitude, don't forget
79
68
  # that Sphinx expects these values to be in radians.
80
- #
69
+ #
81
70
  def initialize(source, columns, options = {})
82
71
  super
83
-
72
+
84
73
  @type = options[:type]
85
74
  @query_source = options[:source]
86
75
  @crc = options[:crc]
87
- @all_ints = options[:all_ints]
88
-
76
+
89
77
  @type ||= :multi unless @query_source.nil?
90
78
  if @type == :string && @crc
91
79
  @type = is_many? ? :multi : :integer
92
80
  end
93
-
81
+
94
82
  source.attributes << self
95
83
  end
96
-
84
+
97
85
  # Get the part of the SELECT clause related to this attribute. Don't forget
98
86
  # to set your model and associations first though.
99
87
  #
100
88
  # This will concatenate strings and arrays of integers, and convert
101
89
  # datetimes to timestamps, as needed.
102
- #
90
+ #
103
91
  def to_select_sql
104
- return nil unless include_as_association? && available?
105
-
92
+ return nil unless include_as_association?
93
+
106
94
  separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
107
-
95
+
108
96
  clause = columns_with_prefixes.collect { |column|
109
97
  case type
110
98
  when :string
@@ -119,28 +107,35 @@ module ThinkingSphinx
119
107
  column
120
108
  end
121
109
  }.join(', ')
122
-
110
+
123
111
  clause = adapter.crc(clause) if @crc
124
112
  clause = adapter.concatenate(clause, separator) if concat_ws?
125
113
  clause = adapter.group_concatenate(clause, separator) if is_many?
126
- clause = adapter.downcase(clause) if insensitive?
127
-
114
+
128
115
  "#{clause} AS #{quote_column(unique_name)}"
129
116
  end
130
-
117
+
131
118
  def type_to_config
132
- SphinxTypeMappings[type]
119
+ {
120
+ :multi => :sql_attr_multi,
121
+ :datetime => :sql_attr_timestamp,
122
+ :string => :sql_attr_str2ordinal,
123
+ :float => :sql_attr_float,
124
+ :boolean => :sql_attr_bool,
125
+ :integer => :sql_attr_uint,
126
+ :bigint => :sql_attr_bigint
127
+ }[type]
133
128
  end
134
-
129
+
135
130
  def include_as_association?
136
131
  ! (type == :multi && (query_source == :query || query_source == :ranged_query))
137
132
  end
138
-
133
+
139
134
  # Returns the configuration value that should be used for
140
135
  # the attribute.
141
136
  # Special case is the multi-valued attribute that needs some
142
- # extra configuration.
143
- #
137
+ # extra configuration.
138
+ #
144
139
  def config_value(offset = nil, delta = false)
145
140
  if type == :multi
146
141
  multi_config = include_as_association? ? "field" :
@@ -150,12 +145,12 @@ module ThinkingSphinx
150
145
  unique_name
151
146
  end
152
147
  end
153
-
148
+
154
149
  # Returns the type of the column. If that's not already set, it returns
155
150
  # :multi if there's the possibility of more than one value, :string if
156
151
  # there's more than one association, otherwise it figures out what the
157
152
  # actual column's datatype is and returns that.
158
- #
153
+ #
159
154
  def type
160
155
  @type ||= begin
161
156
  base_type = case
@@ -166,23 +161,21 @@ module ThinkingSphinx
166
161
  else
167
162
  translated_type_from_database
168
163
  end
169
-
164
+
170
165
  if base_type == :string && @crc
171
166
  base_type = :integer
172
167
  else
173
168
  @crc = false unless base_type == :multi && is_many_strings? && @crc
174
169
  end
175
-
170
+
176
171
  base_type
177
172
  end
178
173
  end
179
-
174
+
180
175
  def updatable?
181
- [:integer, :datetime, :boolean].include?(type) &&
182
- unique_name != :sphinx_internal_id &&
183
- !is_string?
176
+ [:integer, :datetime, :boolean].include?(type) && !is_string?
184
177
  end
185
-
178
+
186
179
  def live_value(instance)
187
180
  object = instance
188
181
  column = @columns.first
@@ -190,29 +183,29 @@ module ThinkingSphinx
190
183
  object = object.send(method)
191
184
  return sphinx_value(nil) if object.nil?
192
185
  }
193
-
186
+
194
187
  sphinx_value object.send(column.__name)
195
188
  end
196
-
189
+
197
190
  def all_ints?
198
- @all_ints || all_of_type?(:integer)
191
+ all_of_type?(:integer)
199
192
  end
200
-
193
+
201
194
  def all_datetimes?
202
195
  all_of_type?(:datetime, :date, :timestamp)
203
196
  end
204
-
197
+
205
198
  def all_strings?
206
199
  all_of_type?(:string, :text)
207
200
  end
208
-
201
+
209
202
  private
210
-
203
+
211
204
  def source_value(offset, delta)
212
205
  if is_string?
213
206
  return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
214
207
  end
215
-
208
+
216
209
  query = query(offset)
217
210
 
218
211
  if query_source == :ranged_query
@@ -220,29 +213,33 @@ module ThinkingSphinx
220
213
  query += " AND #{query_delta.strip}" if delta
221
214
  "ranged-query; #{query}; #{range_query}"
222
215
  else
223
- query += "WHERE #{query_delta.strip}" if delta
216
+ query += " WHERE #{query_delta.strip}" if delta
224
217
  "query; #{query}"
225
218
  end
226
219
  end
227
-
220
+
228
221
  def query(offset)
229
222
  base_assoc = base_association_for_mva
230
223
  end_assoc = end_association_for_mva
231
224
  raise "Could not determine SQL for MVA" if base_assoc.nil?
232
-
233
- <<-SQL
234
- SELECT #{foreign_key_for_mva base_assoc}
235
- #{ThinkingSphinx.unique_id_expression(adapter, offset)} AS #{quote_column('id')},
236
- #{primary_key_for_mva(end_assoc)} AS #{quote_column(unique_name)}
237
- FROM #{quote_table_name base_assoc.table} #{association_joins}
238
- SQL
225
+
226
+ relation = Table(base_assoc.table)
227
+
228
+ association_joins.each do |join|
229
+ relation = relation.join(join.relation, Arel::OuterJoin).
230
+ on(*join.association_join)
231
+ end
232
+
233
+ relation = relation.project "#{foreign_key_for_mva base_assoc} #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')}, #{primary_key_for_mva(end_assoc)} AS #{quote_column(unique_name)}"
234
+
235
+ relation.to_sql
239
236
  end
240
-
237
+
241
238
  def query_clause
242
239
  foreign_key = foreign_key_for_mva base_association_for_mva
243
- "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
240
+ " WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
244
241
  end
245
-
242
+
246
243
  def query_delta
247
244
  foreign_key = foreign_key_for_mva base_association_for_mva
248
245
  <<-SQL
@@ -251,63 +248,63 @@ FROM #{model.quoted_table_name}
251
248
  WHERE #{@source.index.delta_object.clause(model, true)})
252
249
  SQL
253
250
  end
254
-
251
+
255
252
  def range_query
256
253
  assoc = base_association_for_mva
257
254
  foreign_key = foreign_key_for_mva assoc
258
255
  "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
259
256
  end
260
-
257
+
261
258
  def primary_key_for_mva(assoc)
262
259
  quote_with_table(
263
260
  assoc.table, assoc.primary_key_from_reflection || columns.first.__name
264
261
  )
265
262
  end
266
-
263
+
267
264
  def foreign_key_for_mva(assoc)
268
265
  quote_with_table assoc.table, assoc.reflection.primary_key_name
269
266
  end
270
-
267
+
271
268
  def end_association_for_mva
272
- @association_for_mva ||= associations[columns.first.__stack].detect { |assoc|
269
+ @association_for_mva ||= associations[columns.first].detect { |assoc|
273
270
  assoc.has_column?(columns.first.__name)
274
271
  }
275
272
  end
276
-
273
+
277
274
  def base_association_for_mva
278
275
  @first_association_for_mva ||= begin
279
276
  assoc = end_association_for_mva
280
277
  while !assoc.parent.nil?
281
278
  assoc = assoc.parent
282
279
  end
283
-
280
+
284
281
  assoc
285
282
  end
286
283
  end
287
-
284
+
288
285
  def association_joins
289
286
  joins = []
290
287
  assoc = end_association_for_mva
291
288
  while assoc != base_association_for_mva
292
- joins << assoc.to_sql
289
+ joins << assoc.join
293
290
  assoc = assoc.parent
294
291
  end
295
-
296
- joins.join(' ')
292
+
293
+ joins
297
294
  end
298
-
295
+
299
296
  def is_many_ints?
300
297
  concat_ws? && all_ints?
301
298
  end
302
-
299
+
303
300
  def is_many_datetimes?
304
301
  is_many? && all_datetimes?
305
302
  end
306
-
303
+
307
304
  def is_many_strings?
308
305
  is_many? && all_strings?
309
306
  end
310
-
307
+
311
308
  def translated_type_from_database
312
309
  case type_from_db = type_from_database
313
310
  when :integer
@@ -329,16 +326,16 @@ block:
329
326
  MESSAGE
330
327
  end
331
328
  end
332
-
329
+
333
330
  def type_from_database
334
331
  column = column_from_db
335
332
  column.nil? ? nil : column.type
336
333
  end
337
-
334
+
338
335
  def integer_type_from_db
339
336
  column = column_from_db
340
337
  return nil if column.nil?
341
-
338
+
342
339
  case column.sql_type
343
340
  when adapter.bigint_pattern
344
341
  :bigint
@@ -346,27 +343,27 @@ block:
346
343
  :integer
347
344
  end
348
345
  end
349
-
346
+
350
347
  def column_from_db
351
- klass = @associations.values.flatten.first ?
348
+ klass = @associations.values.flatten.first ?
352
349
  @associations.values.flatten.first.reflection.klass : @model
353
-
350
+
354
351
  klass.columns.detect { |col|
355
352
  @columns.collect { |c| c.__name.to_s }.include? col.name
356
353
  }
357
354
  end
358
-
355
+
359
356
  def all_of_type?(*column_types)
360
357
  @columns.all? { |col|
361
- klasses = @associations[col.__stack].empty? ? [@model] :
362
- @associations[col.__stack].collect { |assoc| assoc.reflection.klass }
358
+ klasses = @associations[col].empty? ? [@model] :
359
+ @associations[col].collect { |assoc| assoc.reflection.klass }
363
360
  klasses.all? { |klass|
364
361
  column = klass.columns.detect { |column| column.name == col.__name.to_s }
365
362
  !column.nil? && column_types.include?(column.type)
366
363
  }
367
364
  }
368
365
  end
369
-
366
+
370
367
  def sphinx_value(value)
371
368
  case value
372
369
  when TrueClass
@@ -383,9 +380,5 @@ block:
383
380
  value
384
381
  end
385
382
  end
386
-
387
- def insensitive?
388
- @sortable == :insensitive
389
- end
390
383
  end
391
384
  end