thinking-sphinx 1.5.0 → 2.0.0.rc1

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 (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