warp-thinking-sphinx 1.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (190) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +144 -0
  3. data/VERSION.yml +4 -0
  4. data/features/a.rb +17 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +33 -0
  8. data/features/datetime_deltas.feature +66 -0
  9. data/features/delayed_delta_indexing.feature +37 -0
  10. data/features/deleting_instances.feature +64 -0
  11. data/features/direct_attributes.feature +11 -0
  12. data/features/excerpts.feature +13 -0
  13. data/features/extensible_delta_indexing.feature +9 -0
  14. data/features/facets.feature +76 -0
  15. data/features/facets_across_model.feature +29 -0
  16. data/features/handling_edits.feature +92 -0
  17. data/features/retry_stale_indexes.feature +24 -0
  18. data/features/searching_across_models.feature +20 -0
  19. data/features/searching_by_model.feature +175 -0
  20. data/features/searching_with_find_arguments.feature +56 -0
  21. data/features/sphinx_detection.feature +25 -0
  22. data/features/sphinx_scopes.feature +35 -0
  23. data/features/step_definitions/alpha_steps.rb +3 -0
  24. data/features/step_definitions/beta_steps.rb +7 -0
  25. data/features/step_definitions/common_steps.rb +178 -0
  26. data/features/step_definitions/datetime_delta_steps.rb +15 -0
  27. data/features/step_definitions/delayed_delta_indexing_steps.rb +7 -0
  28. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  29. data/features/step_definitions/facet_steps.rb +92 -0
  30. data/features/step_definitions/find_arguments_steps.rb +36 -0
  31. data/features/step_definitions/gamma_steps.rb +15 -0
  32. data/features/step_definitions/scope_steps.rb +11 -0
  33. data/features/step_definitions/search_steps.rb +89 -0
  34. data/features/step_definitions/sphinx_steps.rb +31 -0
  35. data/features/sti_searching.feature +14 -0
  36. data/features/support/db/active_record.rb +40 -0
  37. data/features/support/db/database.example.yml +3 -0
  38. data/features/support/db/fixtures/alphas.rb +10 -0
  39. data/features/support/db/fixtures/authors.rb +1 -0
  40. data/features/support/db/fixtures/betas.rb +10 -0
  41. data/features/support/db/fixtures/boxes.rb +9 -0
  42. data/features/support/db/fixtures/categories.rb +1 -0
  43. data/features/support/db/fixtures/cats.rb +3 -0
  44. data/features/support/db/fixtures/comments.rb +24 -0
  45. data/features/support/db/fixtures/delayed_betas.rb +10 -0
  46. data/features/support/db/fixtures/developers.rb +29 -0
  47. data/features/support/db/fixtures/dogs.rb +3 -0
  48. data/features/support/db/fixtures/extensible_betas.rb +10 -0
  49. data/features/support/db/fixtures/gammas.rb +10 -0
  50. data/features/support/db/fixtures/people.rb +1001 -0
  51. data/features/support/db/fixtures/posts.rb +6 -0
  52. data/features/support/db/fixtures/robots.rb +14 -0
  53. data/features/support/db/fixtures/tags.rb +27 -0
  54. data/features/support/db/fixtures/thetas.rb +10 -0
  55. data/features/support/db/migrations/create_alphas.rb +7 -0
  56. data/features/support/db/migrations/create_animals.rb +5 -0
  57. data/features/support/db/migrations/create_authors.rb +3 -0
  58. data/features/support/db/migrations/create_authors_posts.rb +6 -0
  59. data/features/support/db/migrations/create_betas.rb +5 -0
  60. data/features/support/db/migrations/create_boxes.rb +5 -0
  61. data/features/support/db/migrations/create_categories.rb +3 -0
  62. data/features/support/db/migrations/create_comments.rb +10 -0
  63. data/features/support/db/migrations/create_delayed_betas.rb +17 -0
  64. data/features/support/db/migrations/create_developers.rb +9 -0
  65. data/features/support/db/migrations/create_extensible_betas.rb +5 -0
  66. data/features/support/db/migrations/create_gammas.rb +3 -0
  67. data/features/support/db/migrations/create_people.rb +13 -0
  68. data/features/support/db/migrations/create_posts.rb +5 -0
  69. data/features/support/db/migrations/create_robots.rb +5 -0
  70. data/features/support/db/migrations/create_taggings.rb +5 -0
  71. data/features/support/db/migrations/create_tags.rb +4 -0
  72. data/features/support/db/migrations/create_thetas.rb +5 -0
  73. data/features/support/db/mysql.rb +3 -0
  74. data/features/support/db/postgresql.rb +3 -0
  75. data/features/support/env.rb +6 -0
  76. data/features/support/lib/generic_delta_handler.rb +8 -0
  77. data/features/support/models/alpha.rb +10 -0
  78. data/features/support/models/animal.rb +5 -0
  79. data/features/support/models/author.rb +3 -0
  80. data/features/support/models/beta.rb +8 -0
  81. data/features/support/models/box.rb +8 -0
  82. data/features/support/models/cat.rb +3 -0
  83. data/features/support/models/category.rb +4 -0
  84. data/features/support/models/comment.rb +10 -0
  85. data/features/support/models/delayed_beta.rb +7 -0
  86. data/features/support/models/developer.rb +16 -0
  87. data/features/support/models/dog.rb +3 -0
  88. data/features/support/models/extensible_beta.rb +9 -0
  89. data/features/support/models/gamma.rb +5 -0
  90. data/features/support/models/person.rb +23 -0
  91. data/features/support/models/post.rb +20 -0
  92. data/features/support/models/robot.rb +8 -0
  93. data/features/support/models/tag.rb +3 -0
  94. data/features/support/models/tagging.rb +4 -0
  95. data/features/support/models/theta.rb +7 -0
  96. data/features/support/post_database.rb +43 -0
  97. data/features/support/z.rb +19 -0
  98. data/lib/thinking_sphinx.rb +212 -0
  99. data/lib/thinking_sphinx/active_record.rb +306 -0
  100. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  101. data/lib/thinking_sphinx/active_record/delta.rb +87 -0
  102. data/lib/thinking_sphinx/active_record/has_many_association.rb +28 -0
  103. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  104. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  105. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  106. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +136 -0
  107. data/lib/thinking_sphinx/association.rb +243 -0
  108. data/lib/thinking_sphinx/attribute.rb +340 -0
  109. data/lib/thinking_sphinx/class_facet.rb +15 -0
  110. data/lib/thinking_sphinx/configuration.rb +282 -0
  111. data/lib/thinking_sphinx/core/array.rb +7 -0
  112. data/lib/thinking_sphinx/core/string.rb +15 -0
  113. data/lib/thinking_sphinx/deltas.rb +30 -0
  114. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  115. data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
  116. data/lib/thinking_sphinx/deltas/delayed_delta.rb +30 -0
  117. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  118. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  119. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  120. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  121. data/lib/thinking_sphinx/excerpter.rb +22 -0
  122. data/lib/thinking_sphinx/facet.rb +125 -0
  123. data/lib/thinking_sphinx/facet_search.rb +134 -0
  124. data/lib/thinking_sphinx/field.rb +81 -0
  125. data/lib/thinking_sphinx/index.rb +99 -0
  126. data/lib/thinking_sphinx/index/builder.rb +286 -0
  127. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  128. data/lib/thinking_sphinx/property.rb +163 -0
  129. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  130. data/lib/thinking_sphinx/search.rb +708 -0
  131. data/lib/thinking_sphinx/search_methods.rb +421 -0
  132. data/lib/thinking_sphinx/source.rb +152 -0
  133. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  134. data/lib/thinking_sphinx/source/sql.rb +127 -0
  135. data/lib/thinking_sphinx/tasks.rb +165 -0
  136. data/rails/init.rb +14 -0
  137. data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +130 -0
  138. data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +49 -0
  139. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  140. data/spec/lib/thinking_sphinx/active_record_spec.rb +353 -0
  141. data/spec/lib/thinking_sphinx/association_spec.rb +239 -0
  142. data/spec/lib/thinking_sphinx/attribute_spec.rb +507 -0
  143. data/spec/lib/thinking_sphinx/configuration_spec.rb +268 -0
  144. data/spec/lib/thinking_sphinx/core/array_spec.rb +9 -0
  145. data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
  146. data/spec/lib/thinking_sphinx/deltas/job_spec.rb +32 -0
  147. data/spec/lib/thinking_sphinx/excerpter_spec.rb +57 -0
  148. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  149. data/spec/lib/thinking_sphinx/facet_spec.rb +333 -0
  150. data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
  151. data/spec/lib/thinking_sphinx/index/builder_spec.rb +455 -0
  152. data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
  153. data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
  154. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
  155. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  156. data/spec/lib/thinking_sphinx/search_spec.rb +1101 -0
  157. data/spec/lib/thinking_sphinx/source_spec.rb +227 -0
  158. data/spec/lib/thinking_sphinx_spec.rb +162 -0
  159. data/tasks/distribution.rb +55 -0
  160. data/tasks/rails.rake +1 -0
  161. data/tasks/testing.rb +83 -0
  162. data/vendor/after_commit/LICENSE +20 -0
  163. data/vendor/after_commit/README +16 -0
  164. data/vendor/after_commit/Rakefile +22 -0
  165. data/vendor/after_commit/init.rb +8 -0
  166. data/vendor/after_commit/lib/after_commit.rb +45 -0
  167. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  168. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  169. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  170. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  171. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  172. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  173. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  174. data/vendor/riddle/lib/riddle.rb +30 -0
  175. data/vendor/riddle/lib/riddle/client.rb +635 -0
  176. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  177. data/vendor/riddle/lib/riddle/client/message.rb +66 -0
  178. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  179. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  180. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  181. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  182. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  183. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  184. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  185. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  186. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  187. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  188. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  189. data/vendor/riddle/lib/riddle/controller.rb +53 -0
  190. metadata +267 -0
@@ -0,0 +1,340 @@
1
+ module ThinkingSphinx
2
+ # Attributes - eternally useful when it comes to filtering, sorting or
3
+ # grouping. This class isn't really useful to you unless you're hacking
4
+ # around with the internals of Thinking Sphinx - but hey, don't let that
5
+ # stop you.
6
+ #
7
+ # One key thing to remember - if you're using the attribute manually to
8
+ # generate SQL statements, you'll need to set the base model, and all the
9
+ # associations. Which can get messy. Use Index.link!, it really helps.
10
+ #
11
+ class Attribute < ThinkingSphinx::Property
12
+ attr_accessor :query_source
13
+
14
+ # To create a new attribute, you'll need to pass in either a single Column
15
+ # or an array of them, and some (optional) options.
16
+ #
17
+ # Valid options are:
18
+ # - :as => :alias_name
19
+ # - :type => :attribute_type
20
+ # - :source => :field, :query, :ranged_query
21
+ #
22
+ # Alias is only required in three circumstances: when there's
23
+ # another attribute or field with the same name, when the column name is
24
+ # 'id', or when there's more than one column.
25
+ #
26
+ # Type is not required, unless you want to force a column to be a certain
27
+ # type (but keep in mind the value will not be CASTed in the SQL
28
+ # statements). The only time you really need to use this is when the type
29
+ # can't be figured out by the column - ie: when not actually using a
30
+ # database column as your source.
31
+ #
32
+ # Source is only used for multi-value attributes (MVA). By default this will
33
+ # use a left-join and a group_concat to obtain the values. For better performance
34
+ # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
35
+ # all document,value-pairs.
36
+ # Either :query or :ranged_query will enable this feature, where :ranged_query will cause
37
+ # the query to be executed incremental.
38
+ #
39
+ # Example usage:
40
+ #
41
+ # Attribute.new(
42
+ # Column.new(:created_at)
43
+ # )
44
+ #
45
+ # Attribute.new(
46
+ # Column.new(:posts, :id),
47
+ # :as => :post_ids
48
+ # )
49
+ #
50
+ # Attribute.new(
51
+ # Column.new(:posts, :id),
52
+ # :as => :post_ids,
53
+ # :source => :ranged_query
54
+ # )
55
+ #
56
+ # Attribute.new(
57
+ # [Column.new(:pages, :id), Column.new(:articles, :id)],
58
+ # :as => :content_ids
59
+ # )
60
+ #
61
+ # Attribute.new(
62
+ # Column.new("NOW()"),
63
+ # :as => :indexed_at,
64
+ # :type => :datetime
65
+ # )
66
+ #
67
+ # If you're creating attributes for latitude and longitude, don't forget
68
+ # that Sphinx expects these values to be in radians.
69
+ #
70
+ def initialize(source, columns, options = {})
71
+ super
72
+
73
+ @type = options[:type]
74
+ @query_source = options[:source]
75
+ @crc = options[:crc]
76
+
77
+ @type ||= :multi unless @query_source.nil?
78
+ if @type == :string && @crc
79
+ @type = is_many? ? :multi : :integer
80
+ end
81
+
82
+ source.attributes << self
83
+ end
84
+
85
+ # Get the part of the SELECT clause related to this attribute. Don't forget
86
+ # to set your model and associations first though.
87
+ #
88
+ # This will concatenate strings and arrays of integers, and convert
89
+ # datetimes to timestamps, as needed.
90
+ #
91
+ def to_select_sql
92
+ return nil unless include_as_association?
93
+
94
+ separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
95
+
96
+ clause = @columns.collect { |column|
97
+ part = column_with_prefix(column)
98
+ case type
99
+ when :string
100
+ adapter.convert_nulls(part)
101
+ when :datetime
102
+ adapter.cast_to_datetime(part)
103
+ when :multi
104
+ part = adapter.cast_to_datetime(part) if is_many_datetimes?
105
+ adapter.convert_nulls(part, 0)
106
+ else
107
+ part
108
+ end
109
+ }.join(', ')
110
+
111
+ # clause = adapter.cast_to_datetime(clause) if type == :datetime
112
+ clause = adapter.crc(clause) if @crc
113
+ clause = adapter.concatenate(clause, separator) if concat_ws?
114
+
115
+ "#{clause} AS #{quote_column(unique_name)}"
116
+ end
117
+
118
+ def type_to_config
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
+ }[type]
127
+ end
128
+
129
+ def include_as_association?
130
+ ! (type == :multi && (query_source == :query || query_source == :ranged_query))
131
+ end
132
+
133
+ # Returns the configuration value that should be used for
134
+ # the attribute.
135
+ # Special case is the multi-valued attribute that needs some
136
+ # extra configuration.
137
+ #
138
+ def config_value(offset = nil, delta = false)
139
+ if type == :multi
140
+ multi_config = include_as_association? ? "field" :
141
+ source_value(offset, delta).gsub(/\s+/m, " ").strip
142
+ "uint #{unique_name} from #{multi_config}"
143
+ else
144
+ unique_name
145
+ end
146
+ end
147
+
148
+ # Returns the type of the column. If that's not already set, it returns
149
+ # :multi if there's the possibility of more than one value, :string if
150
+ # there's more than one association, otherwise it figures out what the
151
+ # actual column's datatype is and returns that.
152
+ #
153
+ def type
154
+ @type ||= begin
155
+ base_type = case
156
+ when is_many?, is_many_ints?
157
+ :multi
158
+ when @associations.values.flatten.length > 1
159
+ :string
160
+ else
161
+ translated_type_from_database
162
+ end
163
+
164
+ if base_type == :string && @crc
165
+ base_type = :integer
166
+ else
167
+ @crc = false unless base_type == :multi && is_many_strings? && @crc
168
+ end
169
+
170
+ base_type
171
+ end
172
+ end
173
+
174
+ def updatable?
175
+ [:integer, :datetime, :boolean].include?(type) && !is_string?
176
+ end
177
+
178
+ def live_value(instance)
179
+ object = instance
180
+ column = @columns.first
181
+ column.__stack.each { |method| object = object.send(method) }
182
+ object.send(column.__name)
183
+ end
184
+
185
+ def all_ints?
186
+ all_of_type?(:integer)
187
+ end
188
+
189
+ def all_datetimes?
190
+ all_of_type?(:datetime, :date, :timestamp)
191
+ end
192
+
193
+ def all_strings?
194
+ all_of_type?(:string, :text)
195
+ end
196
+
197
+ private
198
+
199
+ def source_value(offset, delta)
200
+ if is_string?
201
+ return "#{query_source.to_s.dasherize}; #{columns.first.__name}"
202
+ end
203
+
204
+ query = query(offset)
205
+
206
+ if query_source == :ranged_query
207
+ query += query_clause
208
+ query += " AND #{query_delta.strip}" if delta
209
+ "ranged-query; #{query}; #{range_query}"
210
+ else
211
+ query += "WHERE #{query_delta.strip}" if delta
212
+ "query; #{query}"
213
+ end
214
+ end
215
+
216
+ def query(offset)
217
+ base_assoc = base_association_for_mva
218
+ end_assoc = end_association_for_mva
219
+ raise "Could not determine SQL for MVA" if base_assoc.nil?
220
+
221
+ <<-SQL
222
+ SELECT #{foreign_key_for_mva base_assoc}
223
+ #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')},
224
+ #{primary_key_for_mva(end_assoc)} AS #{quote_column(unique_name)}
225
+ FROM #{quote_table_name base_assoc.table} #{association_joins}
226
+ SQL
227
+ end
228
+
229
+ def query_clause
230
+ foreign_key = foreign_key_for_mva base_association_for_mva
231
+ "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
232
+ end
233
+
234
+ def query_delta
235
+ foreign_key = foreign_key_for_mva base_association_for_mva
236
+ <<-SQL
237
+ #{foreign_key} IN (SELECT #{quote_column model.primary_key}
238
+ FROM #{model.quoted_table_name}
239
+ WHERE #{@source.index.delta_object.clause(model, true)})
240
+ SQL
241
+ end
242
+
243
+ def range_query
244
+ assoc = base_association_for_mva
245
+ foreign_key = foreign_key_for_mva assoc
246
+ "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
247
+ end
248
+
249
+ def primary_key_for_mva(assoc)
250
+ quote_with_table(
251
+ assoc.table, assoc.primary_key_from_reflection || columns.first.__name
252
+ )
253
+ end
254
+
255
+ def foreign_key_for_mva(assoc)
256
+ quote_with_table assoc.table, assoc.reflection.primary_key_name
257
+ end
258
+
259
+ def end_association_for_mva
260
+ @association_for_mva ||= associations[columns.first].detect { |assoc|
261
+ assoc.has_column?(columns.first.__name)
262
+ }
263
+ end
264
+
265
+ def base_association_for_mva
266
+ @first_association_for_mva ||= begin
267
+ assoc = end_association_for_mva
268
+ while !assoc.parent.nil?
269
+ assoc = assoc.parent
270
+ end
271
+
272
+ assoc
273
+ end
274
+ end
275
+
276
+ def association_joins
277
+ joins = []
278
+ assoc = end_association_for_mva
279
+ while assoc != base_association_for_mva
280
+ joins << assoc.to_sql
281
+ assoc = assoc.parent
282
+ end
283
+
284
+ joins.join(' ')
285
+ end
286
+
287
+ def is_many_ints?
288
+ concat_ws? && all_ints?
289
+ end
290
+
291
+ def is_many_datetimes?
292
+ is_many? && all_datetimes?
293
+ end
294
+
295
+ def is_many_strings?
296
+ is_many? && all_strings?
297
+ end
298
+
299
+ def type_from_database
300
+ klass = @associations.values.flatten.first ?
301
+ @associations.values.flatten.first.reflection.klass : @model
302
+
303
+ column = klass.columns.detect { |col|
304
+ @columns.collect { |c| c.__name.to_s }.include? col.name
305
+ }
306
+ column.nil? ? nil : column.type
307
+ end
308
+
309
+ def translated_type_from_database
310
+ case type_from_db = type_from_database
311
+ when :datetime, :string, :float, :boolean, :integer
312
+ type_from_db
313
+ when :decimal
314
+ :float
315
+ when :timestamp, :date
316
+ :datetime
317
+ else
318
+ raise <<-MESSAGE
319
+
320
+ Cannot automatically map attribute #{unique_name} in #{@model.name} to an
321
+ equivalent Sphinx type (integer, float, boolean, datetime, string as ordinal).
322
+ You could try to explicitly convert the column's value in your define_index
323
+ block:
324
+ has "CAST(column AS INT)", :type => :integer, :as => :column
325
+ MESSAGE
326
+ end
327
+ end
328
+
329
+ def all_of_type?(*column_types)
330
+ @columns.all? { |col|
331
+ klasses = @associations[col].empty? ? [@model] :
332
+ @associations[col].collect { |assoc| assoc.reflection.klass }
333
+ klasses.all? { |klass|
334
+ column = klass.columns.detect { |column| column.name == col.__name.to_s }
335
+ !column.nil? && column_types.include?(column.type)
336
+ }
337
+ }
338
+ end
339
+ end
340
+ end
@@ -0,0 +1,15 @@
1
+ module ThinkingSphinx
2
+ class ClassFacet < ThinkingSphinx::Facet
3
+ def name
4
+ :class
5
+ end
6
+
7
+ def attribute_name
8
+ "class_crc"
9
+ end
10
+
11
+ def value(object, attribute_value)
12
+ object.class.name
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,282 @@
1
+ require 'erb'
2
+ require 'singleton'
3
+
4
+ module ThinkingSphinx
5
+ # This class both keeps track of the configuration settings for Sphinx and
6
+ # also generates the resulting file for Sphinx to use.
7
+ #
8
+ # Here are the default settings, relative to RAILS_ROOT where relevant:
9
+ #
10
+ # config file:: config/#{environment}.sphinx.conf
11
+ # searchd log file:: log/searchd.log
12
+ # query log file:: log/searchd.query.log
13
+ # pid file:: log/searchd.#{environment}.pid
14
+ # searchd files:: db/sphinx/#{environment}/
15
+ # address:: 127.0.0.1
16
+ # port:: 3312
17
+ # allow star:: false
18
+ # min prefix length:: 1
19
+ # min infix length:: 1
20
+ # mem limit:: 64M
21
+ # max matches:: 1000
22
+ # morphology:: nil
23
+ # charset type:: utf-8
24
+ # charset table:: nil
25
+ # ignore chars:: nil
26
+ # html strip:: false
27
+ # html remove elements:: ''
28
+ # searchd_binary_name:: searchd
29
+ # indexer_binary_name:: indexer
30
+ #
31
+ # If you want to change these settings, create a YAML file at
32
+ # config/sphinx.yml with settings for each environment, in a similar
33
+ # fashion to database.yml - using the following keys: config_file,
34
+ # searchd_log_file, query_log_file, pid_file, searchd_file_path, port,
35
+ # allow_star, enable_star, min_prefix_len, min_infix_len, mem_limit,
36
+ # max_matches, morphology, charset_type, charset_table, ignore_chars,
37
+ # html_strip, html_remove_elements, delayed_job_priority,
38
+ # searchd_binary_name, indexer_binary_name.
39
+ #
40
+ # I think you've got the idea.
41
+ #
42
+ # Each setting in the YAML file is optional - so only put in the ones you
43
+ # want to change.
44
+ #
45
+ # Keep in mind, if for some particular reason you're using a version of
46
+ # Sphinx older than 0.9.8 r871 (that's prior to the proper 0.9.8 release),
47
+ # don't set allow_star to true.
48
+ #
49
+ class Configuration
50
+ include Singleton
51
+
52
+ SourceOptions = %w( mysql_connect_flags sql_range_step sql_query_pre
53
+ sql_query_post sql_ranged_throttle sql_query_post_index )
54
+
55
+ IndexOptions = %w( charset_table charset_type docinfo enable_star
56
+ exceptions html_index_attrs html_remove_elements html_strip ignore_chars
57
+ min_infix_len min_prefix_len min_word_len mlock morphology ngram_chars
58
+ ngram_len phrase_boundary phrase_boundary_step preopen stopwords
59
+ wordforms )
60
+
61
+ CustomOptions = %w( disable_range )
62
+
63
+ attr_accessor :config_file, :searchd_log_file, :query_log_file,
64
+ :pid_file, :searchd_file_path, :address, :port, :allow_star,
65
+ :database_yml_file, :app_root, :bin_path, :model_directories,
66
+ :delayed_job_priority, :searchd_binary_name, :indexer_binary_name
67
+
68
+ attr_accessor :source_options, :index_options
69
+
70
+ attr_reader :environment, :configuration
71
+
72
+ # Load in the configuration settings - this will look for config/sphinx.yml
73
+ # and parse it according to the current environment.
74
+ #
75
+ def initialize(app_root = Dir.pwd)
76
+ self.reset
77
+ end
78
+
79
+ def self.configure(&block)
80
+ yield instance
81
+ instance.reset(instance.app_root)
82
+ end
83
+
84
+ def reset(custom_app_root=nil)
85
+ if custom_app_root
86
+ self.app_root = custom_app_root
87
+ else
88
+ self.app_root = RAILS_ROOT if defined?(RAILS_ROOT)
89
+ self.app_root = Merb.root if defined?(Merb)
90
+ self.app_root ||= app_root
91
+ end
92
+
93
+ @configuration = Riddle::Configuration.new
94
+ @configuration.searchd.address = "127.0.0.1"
95
+ @configuration.searchd.port = 3312
96
+ @configuration.searchd.pid_file = "#{self.app_root}/log/searchd.#{environment}.pid"
97
+ @configuration.searchd.log = "#{self.app_root}/log/searchd.log"
98
+ @configuration.searchd.query_log = "#{self.app_root}/log/searchd.query.log"
99
+
100
+ self.database_yml_file = "#{self.app_root}/config/database.yml"
101
+ self.config_file = "#{self.app_root}/config/#{environment}.sphinx.conf"
102
+ self.searchd_file_path = "#{self.app_root}/db/sphinx/#{environment}"
103
+ self.allow_star = false
104
+ self.bin_path = ""
105
+ self.model_directories = ["#{app_root}/app/models/"] +
106
+ Dir.glob("#{app_root}/vendor/plugins/*/app/models/")
107
+ self.delayed_job_priority = 0
108
+
109
+ self.source_options = {}
110
+ self.index_options = {
111
+ :charset_type => "utf-8"
112
+ }
113
+
114
+ self.searchd_binary_name = "searchd"
115
+ self.indexer_binary_name = "indexer"
116
+
117
+ parse_config
118
+
119
+ self
120
+ end
121
+
122
+ def self.environment
123
+ @@environment ||= (
124
+ defined?(Merb) ? Merb.environment : ENV['RAILS_ENV']
125
+ ) || "development"
126
+ end
127
+
128
+ def environment
129
+ self.class.environment
130
+ end
131
+
132
+ def controller
133
+ @controller ||= Riddle::Controller.new(@configuration, self.config_file)
134
+ end
135
+
136
+ # Generate the config file for Sphinx by using all the settings defined and
137
+ # looping through all the models with indexes to build the relevant
138
+ # indexer and searchd configuration, and sources and indexes details.
139
+ #
140
+ def build(file_path=nil)
141
+ load_models
142
+ file_path ||= "#{self.config_file}"
143
+
144
+ @configuration.indexes.clear
145
+
146
+ ThinkingSphinx.indexed_models.each_with_index do |model, model_index|
147
+ @configuration.indexes.concat model.constantize.to_riddle(model_index)
148
+ end
149
+
150
+ open(file_path, "w") do |file|
151
+ file.write @configuration.render
152
+ end
153
+ end
154
+
155
+ # Make sure all models are loaded - without reloading any that
156
+ # ActiveRecord::Base is already aware of (otherwise we start to hit some
157
+ # messy dependencies issues).
158
+ #
159
+ def load_models
160
+ return if defined?(Rails) &&
161
+ Rails::VERSION::STRING.to_f > 2.1 &&
162
+ Rails.configuration.cache_classes
163
+
164
+ self.model_directories.each do |base|
165
+ Dir["#{base}**/*.rb"].each do |file|
166
+ model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
167
+
168
+ next if model_name.nil?
169
+ next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
170
+ model.name == model_name
171
+ }
172
+
173
+ begin
174
+ model_name.camelize.constantize
175
+ rescue LoadError
176
+ model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
177
+ rescue NameError
178
+ next
179
+ rescue StandardError
180
+ puts "Warning: Error loading #{file}"
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ def address
187
+ @configuration.searchd.address
188
+ end
189
+
190
+ def address=(address)
191
+ @configuration.searchd.address = address
192
+ end
193
+
194
+ def port
195
+ @configuration.searchd.port
196
+ end
197
+
198
+ def port=(port)
199
+ @configuration.searchd.port = port
200
+ end
201
+
202
+ def pid_file
203
+ @configuration.searchd.pid_file
204
+ end
205
+
206
+ def pid_file=(pid_file)
207
+ @configuration.searchd.pid_file = pid_file
208
+ end
209
+
210
+ def searchd_log_file
211
+ @configuration.searchd.log
212
+ end
213
+
214
+ def searchd_log_file=(file)
215
+ @configuration.searchd.log = file
216
+ end
217
+
218
+ def query_log_file
219
+ @configuration.searchd.query_log
220
+ end
221
+
222
+ def query_log_file=(file)
223
+ @configuration.searchd.query_log = file
224
+ end
225
+
226
+ def client
227
+ client = Riddle::Client.new address, port
228
+ client.max_matches = configuration.searchd.max_matches || 1000
229
+ client
230
+ end
231
+
232
+ def models_by_crc
233
+ @models_by_crc ||= begin
234
+ ThinkingSphinx.indexed_models.inject({}) do |hash, model|
235
+ hash[model.constantize.to_crc32] = model
236
+ Object.subclasses_of(model.constantize).each { |subclass|
237
+ hash[subclass.to_crc32] = subclass.name
238
+ }
239
+ hash
240
+ end
241
+ end
242
+ end
243
+
244
+ private
245
+
246
+ # Parse the config/sphinx.yml file - if it exists - then use the attribute
247
+ # accessors to set the appropriate values. Nothing too clever.
248
+ #
249
+ def parse_config
250
+ path = "#{app_root}/config/sphinx.yml"
251
+ return unless File.exists?(path)
252
+
253
+ conf = YAML::load(ERB.new(IO.read(path)).result)[environment]
254
+
255
+ conf.each do |key,value|
256
+ self.send("#{key}=", value) if self.respond_to?("#{key}=")
257
+
258
+ set_sphinx_setting self.source_options, key, value, SourceOptions
259
+ set_sphinx_setting self.index_options, key, value, IndexOptions
260
+ set_sphinx_setting self.index_options, key, value, CustomOptions
261
+ set_sphinx_setting @configuration.searchd, key, value
262
+ set_sphinx_setting @configuration.indexer, key, value
263
+ end unless conf.nil?
264
+
265
+ self.bin_path += '/' unless self.bin_path.blank?
266
+
267
+ if self.allow_star
268
+ self.index_options[:enable_star] = true
269
+ self.index_options[:min_prefix_len] = 1
270
+ end
271
+ end
272
+
273
+ def set_sphinx_setting(object, key, value, allowed = {})
274
+ if object.is_a?(Hash)
275
+ object[key.to_sym] = value if allowed.include?(key.to_s)
276
+ else
277
+ object.send("#{key}=", value) if object.respond_to?("#{key}")
278
+ send("#{key}=", value) if self.respond_to?("#{key}")
279
+ end
280
+ end
281
+ end
282
+ end