sherpa99-thinking-sphinx 1.1.4

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 (145) hide show
  1. data/LICENCE +20 -0
  2. data/README +107 -0
  3. data/README.textile +107 -0
  4. data/Rakefile +4 -0
  5. data/contribute.rb +328 -0
  6. data/cucumber.yml +1 -0
  7. data/features/a.rb +17 -0
  8. data/features/attribute_transformation.feature +22 -0
  9. data/features/datetime_deltas.feature +55 -0
  10. data/features/delayed_delta_indexing.feature +37 -0
  11. data/features/deleting_instances.feature +52 -0
  12. data/features/facets.feature +26 -0
  13. data/features/handling_edits.feature +67 -0
  14. data/features/retry_stale_indexes.feature +24 -0
  15. data/features/searching_across_models.feature +20 -0
  16. data/features/searching_by_model.feature +118 -0
  17. data/features/searching_with_find_arguments.feature +56 -0
  18. data/features/sphinx_detection.feature +16 -0
  19. data/features/step_definitions/alpha_steps.rb +3 -0
  20. data/features/step_definitions/beta_steps.rb +11 -0
  21. data/features/step_definitions/cat_steps.rb +3 -0
  22. data/features/step_definitions/common_steps.rb +154 -0
  23. data/features/step_definitions/datetime_delta_steps.rb +11 -0
  24. data/features/step_definitions/delayed_delta_indexing_steps.rb +7 -0
  25. data/features/step_definitions/facet_steps.rb +30 -0
  26. data/features/step_definitions/find_arguments_steps.rb +36 -0
  27. data/features/step_definitions/gamma_steps.rb +15 -0
  28. data/features/step_definitions/search_steps.rb +66 -0
  29. data/features/step_definitions/sphinx_steps.rb +23 -0
  30. data/features/support/db/active_record.rb +40 -0
  31. data/features/support/db/database.example.yml +4 -0
  32. data/features/support/db/migrations/create_alphas.rb +18 -0
  33. data/features/support/db/migrations/create_animals.rb +9 -0
  34. data/features/support/db/migrations/create_betas.rb +15 -0
  35. data/features/support/db/migrations/create_boxes.rb +13 -0
  36. data/features/support/db/migrations/create_comments.rb +13 -0
  37. data/features/support/db/migrations/create_delayed_betas.rb +28 -0
  38. data/features/support/db/migrations/create_developers.rb +39 -0
  39. data/features/support/db/migrations/create_gammas.rb +14 -0
  40. data/features/support/db/migrations/create_people.rb +1014 -0
  41. data/features/support/db/migrations/create_posts.rb +6 -0
  42. data/features/support/db/migrations/create_thetas.rb +16 -0
  43. data/features/support/db/mysql.rb +4 -0
  44. data/features/support/db/postgresql.rb +4 -0
  45. data/features/support/env.rb +6 -0
  46. data/features/support/models/alpha.rb +9 -0
  47. data/features/support/models/animal.rb +5 -0
  48. data/features/support/models/beta.rb +7 -0
  49. data/features/support/models/box.rb +8 -0
  50. data/features/support/models/cat.rb +3 -0
  51. data/features/support/models/comment.rb +3 -0
  52. data/features/support/models/delayed_beta.rb +7 -0
  53. data/features/support/models/developer.rb +8 -0
  54. data/features/support/models/gamma.rb +5 -0
  55. data/features/support/models/person.rb +8 -0
  56. data/features/support/models/post.rb +8 -0
  57. data/features/support/models/theta.rb +7 -0
  58. data/features/support/post_database.rb +37 -0
  59. data/features/support/z.rb +19 -0
  60. data/ginger_scenarios.rb +24 -0
  61. data/init.rb +12 -0
  62. data/lib/thinking_sphinx.rb +144 -0
  63. data/lib/thinking_sphinx/active_record.rb +245 -0
  64. data/lib/thinking_sphinx/active_record/delta.rb +74 -0
  65. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  66. data/lib/thinking_sphinx/active_record/search.rb +57 -0
  67. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +34 -0
  68. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +53 -0
  69. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +129 -0
  70. data/lib/thinking_sphinx/association.rb +144 -0
  71. data/lib/thinking_sphinx/attribute.rb +258 -0
  72. data/lib/thinking_sphinx/collection.rb +142 -0
  73. data/lib/thinking_sphinx/configuration.rb +236 -0
  74. data/lib/thinking_sphinx/core/string.rb +22 -0
  75. data/lib/thinking_sphinx/deltas.rb +22 -0
  76. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  77. data/lib/thinking_sphinx/deltas/default_delta.rb +65 -0
  78. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  79. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  80. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  81. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  82. data/lib/thinking_sphinx/facet.rb +58 -0
  83. data/lib/thinking_sphinx/facet_collection.rb +44 -0
  84. data/lib/thinking_sphinx/field.rb +172 -0
  85. data/lib/thinking_sphinx/index.rb +414 -0
  86. data/lib/thinking_sphinx/index/builder.rb +233 -0
  87. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  88. data/lib/thinking_sphinx/rails_additions.rb +133 -0
  89. data/lib/thinking_sphinx/search.rb +638 -0
  90. data/lib/thinking_sphinx/tasks.rb +128 -0
  91. data/rails/init.rb +6 -0
  92. data/spec/fixtures/data.sql +32 -0
  93. data/spec/fixtures/database.yml.default +3 -0
  94. data/spec/fixtures/models.rb +81 -0
  95. data/spec/fixtures/structure.sql +84 -0
  96. data/spec/spec_helper.rb +54 -0
  97. data/spec/sphinx_helper.rb +109 -0
  98. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -0
  99. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  100. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  101. data/spec/unit/thinking_sphinx/active_record_spec.rb +256 -0
  102. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  103. data/spec/unit/thinking_sphinx/attribute_spec.rb +212 -0
  104. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  105. data/spec/unit/thinking_sphinx/configuration_spec.rb +136 -0
  106. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  107. data/spec/unit/thinking_sphinx/field_spec.rb +145 -0
  108. data/spec/unit/thinking_sphinx/index/builder_spec.rb +5 -0
  109. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
  110. data/spec/unit/thinking_sphinx/index_spec.rb +54 -0
  111. data/spec/unit/thinking_sphinx/search_spec.rb +59 -0
  112. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  113. data/tasks/distribution.rb +48 -0
  114. data/tasks/rails.rake +1 -0
  115. data/tasks/testing.rb +86 -0
  116. data/thinking-sphinx.gemspec +232 -0
  117. data/vendor/after_commit/LICENSE +20 -0
  118. data/vendor/after_commit/README +16 -0
  119. data/vendor/after_commit/Rakefile +22 -0
  120. data/vendor/after_commit/init.rb +5 -0
  121. data/vendor/after_commit/lib/after_commit.rb +42 -0
  122. data/vendor/after_commit/lib/after_commit/active_record.rb +91 -0
  123. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  124. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  125. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  126. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  127. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  128. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  129. data/vendor/riddle/lib/riddle.rb +30 -0
  130. data/vendor/riddle/lib/riddle/client.rb +619 -0
  131. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  132. data/vendor/riddle/lib/riddle/client/message.rb +65 -0
  133. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  134. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  135. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  136. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  137. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  138. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  139. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  140. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  141. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  142. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  143. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  144. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  145. metadata +248 -0
@@ -0,0 +1,414 @@
1
+ require 'thinking_sphinx/index/builder'
2
+ require 'thinking_sphinx/index/faux_column'
3
+
4
+ module ThinkingSphinx
5
+ # The Index class is a ruby representation of a Sphinx source (not a Sphinx
6
+ # index - yes, I know it's a little confusing. You'll manage). This is
7
+ # another 'internal' Thinking Sphinx class - if you're using it directly,
8
+ # you either know what you're doing, or messing with things beyond your ken.
9
+ # Enjoy.
10
+ #
11
+ class Index
12
+ attr_accessor :model, :fields, :attributes, :conditions, :groupings,
13
+ :delta_object, :options
14
+
15
+ # Create a new index instance by passing in the model it is tied to, and
16
+ # a block to build it with (optional but recommended). For documentation
17
+ # on the syntax for inside the block, the Builder class is what you want.
18
+ #
19
+ # Quick Example:
20
+ #
21
+ # Index.new(User) do
22
+ # indexes login, email
23
+ #
24
+ # has created_at
25
+ #
26
+ # set_property :delta => true
27
+ # end
28
+ #
29
+ def initialize(model, &block)
30
+ @model = model
31
+ @associations = {}
32
+ @fields = []
33
+ @attributes = []
34
+ @conditions = []
35
+ @groupings = []
36
+ @options = {}
37
+ @delta_object = nil
38
+
39
+ initialize_from_builder(&block) if block_given?
40
+ end
41
+
42
+ def name
43
+ self.class.name(@model)
44
+ end
45
+
46
+ def self.name(model)
47
+ model.name.underscore.tr(':/\\', '_')
48
+ end
49
+
50
+ def to_riddle_for_core(offset, index)
51
+ add_internal_attributes
52
+ link!
53
+
54
+ source = Riddle::Configuration::SQLSource.new(
55
+ "#{name}_core_#{index}", adapter.sphinx_identifier
56
+ )
57
+
58
+ set_source_database_settings source
59
+ set_source_attributes source
60
+ set_source_sql source, offset
61
+ set_source_settings source
62
+
63
+ source
64
+ end
65
+
66
+ def to_riddle_for_delta(offset, index)
67
+ add_internal_attributes
68
+ link!
69
+
70
+ source = Riddle::Configuration::SQLSource.new(
71
+ "#{name}_delta_#{index}", adapter.sphinx_identifier
72
+ )
73
+ source.parent = "#{name}_core_#{index}"
74
+
75
+ set_source_database_settings source
76
+ set_source_attributes source
77
+ set_source_sql source, offset, true
78
+
79
+ source
80
+ end
81
+
82
+ # Link all the fields and associations to their corresponding
83
+ # associations and joins. This _must_ be called before interrogating
84
+ # the index's fields and associations for anything that may reference
85
+ # their SQL structure.
86
+ #
87
+ def link!
88
+ base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(
89
+ @model, [], nil
90
+ )
91
+
92
+ @fields.each { |field|
93
+ field.model ||= @model
94
+ field.columns.each { |col|
95
+ field.associations[col] = associations(col.__stack.clone)
96
+ field.associations[col].each { |assoc| assoc.join_to(base) }
97
+ }
98
+ }
99
+
100
+ @attributes.each { |attribute|
101
+ attribute.model ||= @model
102
+ attribute.columns.each { |col|
103
+ attribute.associations[col] = associations(col.__stack.clone)
104
+ attribute.associations[col].each { |assoc| assoc.join_to(base) }
105
+ }
106
+ }
107
+ end
108
+
109
+ # Flag to indicate whether this index has a corresponding delta index.
110
+ #
111
+ def delta?
112
+ !@delta_object.nil?
113
+ end
114
+
115
+ def adapter
116
+ @adapter ||= @model.sphinx_database_adapter
117
+ end
118
+
119
+ def prefix_fields
120
+ @fields.select { |field| field.prefixes }
121
+ end
122
+
123
+ def infix_fields
124
+ @fields.select { |field| field.infixes }
125
+ end
126
+
127
+ def index_options
128
+ all_index_options = ThinkingSphinx::Configuration.instance.index_options.clone
129
+ @options.keys.select { |key|
130
+ ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s)
131
+ }.each { |key| all_index_options[key.to_sym] = @options[key] }
132
+ all_index_options
133
+ end
134
+
135
+ def quote_column(column)
136
+ @model.connection.quote_column_name(column)
137
+ end
138
+
139
+ private
140
+
141
+ def utf8?
142
+ self.index_options[:charset_type] == "utf-8"
143
+ end
144
+
145
+ # Does all the magic with the block provided to the base #initialize.
146
+ # Creates a new class subclassed from Builder, and evaluates the block
147
+ # on it, then pulls all relevant settings - fields, attributes, conditions,
148
+ # properties - into the new index.
149
+ #
150
+ # Also creates a CRC attribute for the model.
151
+ #
152
+ def initialize_from_builder(&block)
153
+ builder = Class.new(Builder)
154
+ builder.setup
155
+
156
+ builder.instance_eval &block
157
+
158
+ unless @model.descends_from_active_record?
159
+ stored_class = @model.store_full_sti_class ? @model.name : @model.name.demodulize
160
+ builder.where("#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'")
161
+ end
162
+
163
+ set_model = Proc.new { |item| item.model = @model }
164
+
165
+ @fields = builder.fields &set_model
166
+ @attributes = builder.attributes.each &set_model
167
+ @conditions = builder.conditions
168
+ @groupings = builder.groupings
169
+ @delta_object = ThinkingSphinx::Deltas.parse self, builder.properties
170
+ @options = builder.properties
171
+
172
+ is_faceted = Proc.new { |item| item.faceted }
173
+ add_facet = Proc.new { |item| @model.sphinx_facets << item.to_facet }
174
+
175
+ @model.sphinx_facets ||= []
176
+ @fields.select( &is_faceted).each &add_facet
177
+ @attributes.select(&is_faceted).each &add_facet
178
+
179
+ # We want to make sure that if the database doesn't exist, then Thinking
180
+ # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
181
+ # and db:migrate). It's a bit hacky, but I can't think of a better way.
182
+ rescue StandardError => err
183
+ case err.class.name
184
+ when "Mysql::Error", "ActiveRecord::StatementInvalid"
185
+ return
186
+ else
187
+ raise err
188
+ end
189
+ end
190
+
191
+ # Returns all associations used amongst all the fields and attributes.
192
+ # This includes all associations between the model and what the actual
193
+ # columns are from.
194
+ #
195
+ def all_associations
196
+ @all_associations ||= (
197
+ # field associations
198
+ @fields.collect { |field|
199
+ field.associations.values
200
+ }.flatten +
201
+ # attribute associations
202
+ @attributes.collect { |attrib|
203
+ attrib.associations.values
204
+ }.flatten
205
+ ).uniq.collect { |assoc|
206
+ # get ancestors as well as column-level associations
207
+ assoc.ancestors
208
+ }.flatten.uniq
209
+ end
210
+
211
+ # Gets a stack of associations for a specific path.
212
+ #
213
+ def associations(path, parent = nil)
214
+ assocs = []
215
+
216
+ if parent.nil?
217
+ assocs = association(path.shift)
218
+ else
219
+ assocs = parent.children(path.shift)
220
+ end
221
+
222
+ until path.empty?
223
+ point = path.shift
224
+ assocs = assocs.collect { |assoc|
225
+ assoc.children(point)
226
+ }.flatten
227
+ end
228
+
229
+ assocs
230
+ end
231
+
232
+ # Gets the association stack for a specific key.
233
+ #
234
+ def association(key)
235
+ @associations[key] ||= Association.children(@model, key)
236
+ end
237
+
238
+ def crc_column
239
+ if @model.column_names.include?(@model.inheritance_column)
240
+ adapter.cast_to_unsigned(adapter.convert_nulls(
241
+ adapter.crc(adapter.quote_with_table(@model.inheritance_column)),
242
+ @model.to_crc32
243
+ ))
244
+ else
245
+ @model.to_crc32.to_s
246
+ end
247
+ end
248
+
249
+ def add_internal_attributes
250
+ @attributes << Attribute.new(
251
+ FauxColumn.new(@model.primary_key.to_sym),
252
+ :type => :integer,
253
+ :as => :sphinx_internal_id
254
+ ) unless @attributes.detect { |attr| attr.alias == :sphinx_internal_id }
255
+
256
+ @attributes << Attribute.new(
257
+ FauxColumn.new(crc_column),
258
+ :type => :integer,
259
+ :as => :class_crc
260
+ ) unless @attributes.detect { |attr| attr.alias == :class_crc }
261
+
262
+ @attributes << Attribute.new(
263
+ FauxColumn.new("'" + (@model.send(:subclasses).collect { |klass|
264
+ klass.to_crc32.to_s
265
+ } << @model.to_crc32.to_s).join(",") + "'"),
266
+ :type => :multi,
267
+ :as => :subclass_crcs
268
+ ) unless @attributes.detect { |attr| attr.alias == :subclass_crcs }
269
+
270
+ @attributes << Attribute.new(
271
+ FauxColumn.new("0"),
272
+ :type => :integer,
273
+ :as => :sphinx_deleted
274
+ ) unless @attributes.detect { |attr| attr.alias == :sphinx_deleted }
275
+ end
276
+
277
+ def set_source_database_settings(source)
278
+ config = @model.connection.instance_variable_get(:@config)
279
+
280
+ source.sql_host = config[:host] || "localhost"
281
+ source.sql_user = config[:username] || config[:user] || ""
282
+ source.sql_pass = (config[:password].to_s || "").gsub('#', '\#')
283
+ source.sql_db = config[:database]
284
+ source.sql_port = config[:port]
285
+ source.sql_sock = config[:socket]
286
+ end
287
+
288
+ def set_source_attributes(source)
289
+ attributes.each do |attrib|
290
+ source.send(attrib.type_to_config) << attrib.config_value
291
+ end
292
+ end
293
+
294
+ def set_source_sql(source, offset, delta = false)
295
+ source.sql_query = to_sql(:offset => offset, :delta => delta).gsub(/\n/, ' ')
296
+ source.sql_query_range = to_sql_query_range(:delta => delta)
297
+ source.sql_query_info = to_sql_query_info(offset)
298
+
299
+ source.sql_query_pre += send(!delta ? :sql_query_pre_for_core : :sql_query_pre_for_delta)
300
+
301
+ if @options[:group_concat_max_len]
302
+ source.sql_query_pre << "SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}"
303
+ end
304
+
305
+ source.sql_query_pre += [adapter.utf8_query_pre].compact if utf8?
306
+ end
307
+
308
+ def set_source_settings(source)
309
+ ThinkingSphinx::Configuration.instance.source_options.each do |key, value|
310
+ source.send("#{key}=".to_sym, value)
311
+ end
312
+
313
+ @options.each do |key, value|
314
+ source.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::SourceOptions.include?(key.to_s) && !value.nil?
315
+ end
316
+ end
317
+
318
+ def sql_query_pre_for_core
319
+ if self.delta? && !@delta_object.reset_query(@model).blank?
320
+ [@delta_object.reset_query(@model)]
321
+ else
322
+ []
323
+ end
324
+ end
325
+
326
+ def sql_query_pre_for_delta
327
+ [""]
328
+ end
329
+
330
+ # Generates the big SQL statement to get the data back for all the fields
331
+ # and attributes, using all the relevant association joins. If you want
332
+ # the version filtered for delta values, send through :delta => true in the
333
+ # options. Won't do much though if the index isn't set up to support a
334
+ # delta sibling.
335
+ #
336
+ # Examples:
337
+ #
338
+ # index.to_sql
339
+ # index.to_sql(:delta => true)
340
+ #
341
+ def to_sql(options={})
342
+ assocs = all_associations
343
+
344
+ where_clause = ""
345
+ if self.delta? && !@delta_object.clause(@model, options[:delta]).blank?
346
+ where_clause << " AND #{@delta_object.clause(@model, options[:delta])}"
347
+ end
348
+ unless @conditions.empty?
349
+ where_clause << " AND " << @conditions.join(" AND ")
350
+ end
351
+
352
+ internal_groupings = []
353
+ if @model.column_names.include?(@model.inheritance_column)
354
+ internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}"
355
+ end
356
+
357
+ unique_id_expr = "* #{ThinkingSphinx.indexed_models.size} + #{options[:offset] || 0}"
358
+
359
+ sql = <<-SQL
360
+ SELECT #{ (
361
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] +
362
+ @fields.collect { |field| field.to_select_sql } +
363
+ @attributes.collect { |attribute| attribute.to_select_sql }
364
+ ).join(", ") }
365
+ FROM #{ @model.table_name }
366
+ #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
367
+ WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
368
+ AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} <= $end
369
+ #{ where_clause }
370
+ GROUP BY #{ (
371
+ ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
372
+ @fields.collect { |field| field.to_group_sql }.compact +
373
+ @attributes.collect { |attribute| attribute.to_group_sql }.compact +
374
+ @groupings + internal_groupings
375
+ ).join(", ") }
376
+ SQL
377
+
378
+ if @model.connection.class.name == "ActiveRecord::ConnectionAdapters::MysqlAdapter"
379
+ sql += " ORDER BY NULL"
380
+ end
381
+
382
+ sql
383
+ end
384
+
385
+ # Simple helper method for the query info SQL - which is a statement that
386
+ # returns the single row for a corresponding id.
387
+ #
388
+ def to_sql_query_info(offset)
389
+ "SELECT * FROM #{@model.quoted_table_name} WHERE " +
390
+ " #{quote_column(@model.primary_key)} = (($id - #{offset}) / #{ThinkingSphinx.indexed_models.size})"
391
+ end
392
+
393
+ # Simple helper method for the query range SQL - which is a statement that
394
+ # returns minimum and maximum id values. These can be filtered by delta -
395
+ # so pass in :delta => true to get the delta version of the SQL.
396
+ #
397
+ def to_sql_query_range(options={})
398
+ min_statement = adapter.convert_nulls(
399
+ "MIN(#{quote_column(@model.primary_key)})", 1
400
+ )
401
+ max_statement = adapter.convert_nulls(
402
+ "MAX(#{quote_column(@model.primary_key)})", 1
403
+ )
404
+
405
+ sql = "SELECT #{min_statement}, #{max_statement} " +
406
+ "FROM #{@model.quoted_table_name} "
407
+ if self.delta? && !@delta_object.clause(@model, options[:delta]).blank?
408
+ sql << "WHERE #{@delta_object.clause(@model, options[:delta])}"
409
+ end
410
+
411
+ sql
412
+ end
413
+ end
414
+ end
@@ -0,0 +1,233 @@
1
+ module ThinkingSphinx
2
+ class Index
3
+ # The Builder class is the core for the index definition block processing.
4
+ # There are four methods you really need to pay attention to:
5
+ # - indexes (aliased to includes and attribute)
6
+ # - has (aliased to attribute)
7
+ # - where
8
+ # - set_property (aliased to set_properties)
9
+ #
10
+ # The first two of these methods allow you to define what data makes up
11
+ # your indexes. #where provides a method to add manual SQL conditions, and
12
+ # set_property allows you to set some settings on a per-index basis. Check
13
+ # out each method's documentation for better ideas of usage.
14
+ #
15
+ class Builder
16
+ class << self
17
+ # No idea where this is coming from - haven't found it in any ruby or
18
+ # rails documentation. It's not needed though, so it gets undef'd.
19
+ # Hopefully the list of methods that get in the way doesn't get too
20
+ # long.
21
+ HiddenMethods = [:parent, :name, :id, :type].each { |method|
22
+ define_method(method) {
23
+ caller.grep(/irb.completion/).empty? ? method_missing(method) : super
24
+ }
25
+ }
26
+
27
+ attr_accessor :fields, :attributes, :properties, :conditions,
28
+ :groupings
29
+
30
+ # Set up all the collections. Consider this the equivalent of an
31
+ # instance's initialize method.
32
+ #
33
+ def setup
34
+ @fields = []
35
+ @attributes = []
36
+ @properties = {}
37
+ @conditions = []
38
+ @groupings = []
39
+ end
40
+
41
+ # This is how you add fields - the strings Sphinx looks at - to your
42
+ # index. Technically, to use this method, you need to pass in some
43
+ # columns and options - but there's some neat method_missing stuff
44
+ # happening, so lets stick to the expected syntax within a define_index
45
+ # block.
46
+ #
47
+ # Expected options are :as, which points to a column alias in symbol
48
+ # form, and :sortable, which indicates whether you want to sort by this
49
+ # field.
50
+ #
51
+ # Adding Single-Column Fields:
52
+ #
53
+ # You can use symbols or methods - and can chain methods together to
54
+ # get access down the associations tree.
55
+ #
56
+ # indexes :id, :as => :my_id
57
+ # indexes :name, :sortable => true
58
+ # indexes first_name, last_name, :sortable => true
59
+ # indexes users.posts.content, :as => :post_content
60
+ # indexes users(:id), :as => :user_ids
61
+ #
62
+ # Keep in mind that if any keywords for Ruby methods - such as id or
63
+ # name - clash with your column names, you need to use the symbol
64
+ # version (see the first, second and last examples above).
65
+ #
66
+ # If you specify multiple columns (example #2), a field will be created
67
+ # for each. Don't use the :as option in this case. If you want to merge
68
+ # those columns together, continue reading.
69
+ #
70
+ # Adding Multi-Column Fields:
71
+ #
72
+ # indexes [first_name, last_name], :as => :name
73
+ # indexes [location, parent.location], :as => :location
74
+ #
75
+ # To combine multiple columns into a single field, you need to wrap
76
+ # them in an Array, as shown by the above examples. There's no
77
+ # limitations on whether they're symbols or methods or what level of
78
+ # associations they come from.
79
+ #
80
+ # Adding SQL Fragment Fields
81
+ #
82
+ # You can also define a field using an SQL fragment, useful for when
83
+ # you would like to index a calculated value.
84
+ #
85
+ # indexes "age < 18", :as => :minor
86
+ #
87
+ def indexes(*args)
88
+ options = args.extract_options!
89
+ args.each do |columns|
90
+ fields << Field.new(FauxColumn.coerce(columns), options)
91
+
92
+ if fields.last.sortable || fields.last.faceted
93
+ attributes << Attribute.new(
94
+ fields.last.columns.collect { |col| col.clone },
95
+ options.merge(
96
+ :type => :string,
97
+ :as => fields.last.unique_name.to_s.concat("_sort").to_sym
98
+ ).except(:facet)
99
+ )
100
+ end
101
+ end
102
+ end
103
+ alias_method :field, :indexes
104
+ alias_method :includes, :indexes
105
+
106
+ # This is the method to add attributes to your index (hence why it is
107
+ # aliased as 'attribute'). The syntax is the same as #indexes, so use
108
+ # that as starting point, but keep in mind the following points.
109
+ #
110
+ # An attribute can have an alias (the :as option), but it is always
111
+ # sortable - so you don't need to explicitly request that. You _can_
112
+ # specify the data type of the attribute (the :type option), but the
113
+ # code's pretty good at figuring that out itself from peering into the
114
+ # database.
115
+ #
116
+ # Attributes are limited to the following types: integers, floats,
117
+ # datetimes (converted to timestamps), booleans and strings. Don't
118
+ # forget that Sphinx converts string attributes to integers, which are
119
+ # useful for sorting, but that's about it.
120
+ #
121
+ # You can also have a collection of integers for multi-value attributes
122
+ # (MVAs). Generally these would be through a has_many relationship,
123
+ # like in this example:
124
+ #
125
+ # has posts(:id), :as => :post_ids
126
+ #
127
+ # This allows you to filter on any of the values tied to a specific
128
+ # record. Might be best to read through the Sphinx documentation to get
129
+ # a better idea of that though.
130
+ #
131
+ # Adding SQL Fragment Attributes
132
+ #
133
+ # You can also define an attribute using an SQL fragment, useful for
134
+ # when you would like to index a calculated value. Don't forget to set
135
+ # the type of the attribute though:
136
+ #
137
+ # has "age < 18", :as => :minor, :type => :boolean
138
+ #
139
+ # If you're creating attributes for latitude and longitude, don't
140
+ # forget that Sphinx expects these values to be in radians.
141
+ #
142
+ def has(*args)
143
+ options = args.extract_options!
144
+ args.each do |columns|
145
+ attributes << Attribute.new(FauxColumn.coerce(columns), options)
146
+ end
147
+ end
148
+ alias_method :attribute, :has
149
+
150
+ def facet(*args)
151
+ options = args.extract_options!
152
+ options[:facet] = true
153
+
154
+ args.each do |columns|
155
+ attributes << Attribute.new(FauxColumn.coerce(columns), options)
156
+ end
157
+ end
158
+
159
+ # Use this method to add some manual SQL conditions for your index
160
+ # request. You can pass in as many strings as you like, they'll get
161
+ # joined together with ANDs later on.
162
+ #
163
+ # where "user_id = 10"
164
+ # where "parent_type = 'Article'", "created_at < NOW()"
165
+ #
166
+ def where(*args)
167
+ @conditions += args
168
+ end
169
+
170
+ # Use this method to add some manual SQL strings to the GROUP BY
171
+ # clause. You can pass in as many strings as you'd like, they'll get
172
+ # joined together with commas later on.
173
+ #
174
+ # group_by "lat", "lng"
175
+ #
176
+ def group_by(*args)
177
+ @groupings += args
178
+ end
179
+
180
+ # This is what to use to set properties on the index. Chief amongst
181
+ # those is the delta property - to allow automatic updates to your
182
+ # indexes as new models are added and edited - but also you can
183
+ # define search-related properties which will be the defaults for all
184
+ # searches on the model.
185
+ #
186
+ # set_property :delta => true
187
+ # set_property :field_weights => {"name" => 100}
188
+ # set_property :order => "name ASC"
189
+ # set_property :include => :picture
190
+ # set_property :select => 'name'
191
+ #
192
+ # Also, the following two properties are particularly relevant for
193
+ # geo-location searching - latitude_attr and longitude_attr. If your
194
+ # attributes for these two values are named something other than
195
+ # lat/latitude or lon/long/longitude, you can dictate what they are
196
+ # when defining the index, so you don't need to specify them for every
197
+ # geo-related search.
198
+ #
199
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
200
+ #
201
+ # Please don't forget to add a boolean field named 'delta' to your
202
+ # model's database table if enabling the delta index for it.
203
+ #
204
+ def set_property(*args)
205
+ options = args.extract_options!
206
+ if options.empty?
207
+ @properties[args[0]] = args[1]
208
+ else
209
+ @properties.merge!(options)
210
+ end
211
+ end
212
+ alias_method :set_properties, :set_property
213
+
214
+ # Handles the generation of new columns for the field and attribute
215
+ # definitions.
216
+ #
217
+ def method_missing(method, *args)
218
+ FauxColumn.new(method, *args)
219
+ end
220
+
221
+ # A method to allow adding fields from associations which have names
222
+ # that clash with method names in the Builder class (ie: properties,
223
+ # fields, attributes).
224
+ #
225
+ # Example: indexes assoc(:properties).column
226
+ #
227
+ def assoc(assoc)
228
+ FauxColumn.new(method)
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end