pixeltrix-thinking-sphinx 1.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/LICENCE +20 -0
  2. data/README +107 -0
  3. data/lib/thinking_sphinx.rb +144 -0
  4. data/lib/thinking_sphinx/active_record.rb +245 -0
  5. data/lib/thinking_sphinx/active_record/delta.rb +74 -0
  6. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  7. data/lib/thinking_sphinx/active_record/search.rb +57 -0
  8. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +34 -0
  9. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +53 -0
  10. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +129 -0
  11. data/lib/thinking_sphinx/association.rb +144 -0
  12. data/lib/thinking_sphinx/attribute.rb +258 -0
  13. data/lib/thinking_sphinx/collection.rb +142 -0
  14. data/lib/thinking_sphinx/configuration.rb +236 -0
  15. data/lib/thinking_sphinx/core/string.rb +22 -0
  16. data/lib/thinking_sphinx/deltas.rb +22 -0
  17. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  18. data/lib/thinking_sphinx/deltas/default_delta.rb +65 -0
  19. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  20. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  22. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  23. data/lib/thinking_sphinx/facet.rb +58 -0
  24. data/lib/thinking_sphinx/facet_collection.rb +44 -0
  25. data/lib/thinking_sphinx/field.rb +172 -0
  26. data/lib/thinking_sphinx/index.rb +414 -0
  27. data/lib/thinking_sphinx/index/builder.rb +233 -0
  28. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  29. data/lib/thinking_sphinx/rails_additions.rb +133 -0
  30. data/lib/thinking_sphinx/search.rb +638 -0
  31. data/lib/thinking_sphinx/tasks.rb +128 -0
  32. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -0
  33. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  34. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  35. data/spec/unit/thinking_sphinx/active_record_spec.rb +256 -0
  36. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  37. data/spec/unit/thinking_sphinx/attribute_spec.rb +212 -0
  38. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  39. data/spec/unit/thinking_sphinx/configuration_spec.rb +136 -0
  40. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  41. data/spec/unit/thinking_sphinx/field_spec.rb +145 -0
  42. data/spec/unit/thinking_sphinx/index/builder_spec.rb +5 -0
  43. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
  44. data/spec/unit/thinking_sphinx/index_spec.rb +54 -0
  45. data/spec/unit/thinking_sphinx/search_spec.rb +59 -0
  46. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  47. data/tasks/distribution.rb +48 -0
  48. data/tasks/rails.rake +1 -0
  49. data/tasks/testing.rb +86 -0
  50. data/vendor/after_commit/LICENSE +20 -0
  51. data/vendor/after_commit/README +16 -0
  52. data/vendor/after_commit/Rakefile +22 -0
  53. data/vendor/after_commit/init.rb +5 -0
  54. data/vendor/after_commit/lib/after_commit.rb +42 -0
  55. data/vendor/after_commit/lib/after_commit/active_record.rb +91 -0
  56. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  57. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  58. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  59. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  60. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  61. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  62. data/vendor/riddle/lib/riddle.rb +30 -0
  63. data/vendor/riddle/lib/riddle/client.rb +619 -0
  64. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  65. data/vendor/riddle/lib/riddle/client/message.rb +65 -0
  66. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  67. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  68. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  69. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  70. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  71. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  72. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  73. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  74. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  75. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  76. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  77. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  78. metadata +157 -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