initforthe-thinking-sphinx 1.1.21

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 (91) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +141 -0
  3. data/lib/thinking_sphinx.rb +215 -0
  4. data/lib/thinking_sphinx/active_record.rb +278 -0
  5. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  6. data/lib/thinking_sphinx/active_record/delta.rb +87 -0
  7. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  8. data/lib/thinking_sphinx/active_record/search.rb +57 -0
  9. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  10. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  11. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +135 -0
  12. data/lib/thinking_sphinx/association.rb +164 -0
  13. data/lib/thinking_sphinx/attribute.rb +268 -0
  14. data/lib/thinking_sphinx/class_facet.rb +15 -0
  15. data/lib/thinking_sphinx/collection.rb +148 -0
  16. data/lib/thinking_sphinx/configuration.rb +262 -0
  17. data/lib/thinking_sphinx/core/string.rb +15 -0
  18. data/lib/thinking_sphinx/deltas.rb +30 -0
  19. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  20. data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta.rb +27 -0
  22. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  23. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  24. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  25. data/lib/thinking_sphinx/deploy/capistrano.rb +82 -0
  26. data/lib/thinking_sphinx/facet.rb +108 -0
  27. data/lib/thinking_sphinx/facet_collection.rb +59 -0
  28. data/lib/thinking_sphinx/field.rb +82 -0
  29. data/lib/thinking_sphinx/index.rb +99 -0
  30. data/lib/thinking_sphinx/index/builder.rb +287 -0
  31. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  32. data/lib/thinking_sphinx/property.rb +160 -0
  33. data/lib/thinking_sphinx/rails_additions.rb +136 -0
  34. data/lib/thinking_sphinx/search.rb +727 -0
  35. data/lib/thinking_sphinx/search/facets.rb +104 -0
  36. data/lib/thinking_sphinx/source.rb +150 -0
  37. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  38. data/lib/thinking_sphinx/source/sql.rb +126 -0
  39. data/lib/thinking_sphinx/tasks.rb +162 -0
  40. data/rails/init.rb +14 -0
  41. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -0
  42. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  43. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  44. data/spec/unit/thinking_sphinx/active_record_spec.rb +329 -0
  45. data/spec/unit/thinking_sphinx/association_spec.rb +246 -0
  46. data/spec/unit/thinking_sphinx/attribute_spec.rb +338 -0
  47. data/spec/unit/thinking_sphinx/collection_spec.rb +15 -0
  48. data/spec/unit/thinking_sphinx/configuration_spec.rb +222 -0
  49. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  50. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
  51. data/spec/unit/thinking_sphinx/facet_spec.rb +302 -0
  52. data/spec/unit/thinking_sphinx/field_spec.rb +154 -0
  53. data/spec/unit/thinking_sphinx/index/builder_spec.rb +355 -0
  54. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
  55. data/spec/unit/thinking_sphinx/index_spec.rb +45 -0
  56. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +191 -0
  57. data/spec/unit/thinking_sphinx/search_spec.rb +228 -0
  58. data/spec/unit/thinking_sphinx/source_spec.rb +217 -0
  59. data/spec/unit/thinking_sphinx_spec.rb +151 -0
  60. data/tasks/distribution.rb +67 -0
  61. data/tasks/rails.rake +1 -0
  62. data/tasks/testing.rb +78 -0
  63. data/vendor/after_commit/LICENSE +20 -0
  64. data/vendor/after_commit/README +16 -0
  65. data/vendor/after_commit/Rakefile +22 -0
  66. data/vendor/after_commit/init.rb +8 -0
  67. data/vendor/after_commit/lib/after_commit.rb +45 -0
  68. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  69. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  70. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  71. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  72. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  73. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  74. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  75. data/vendor/riddle/lib/riddle.rb +30 -0
  76. data/vendor/riddle/lib/riddle/client.rb +619 -0
  77. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  78. data/vendor/riddle/lib/riddle/client/message.rb +65 -0
  79. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  80. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  81. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  82. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  83. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  84. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  85. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  86. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  87. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  88. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  89. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  90. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  91. metadata +190 -0
@@ -0,0 +1,164 @@
1
+ module ThinkingSphinx
2
+ # Association tracks a specific reflection and join to reference data that
3
+ # isn't in the base model. Very much an internal class for Thinking Sphinx -
4
+ # perhaps because I feel it's not as strong (or simple) as most of the rest.
5
+ #
6
+ class Association
7
+ attr_accessor :parent, :reflection, :join
8
+
9
+ # Create a new association by passing in the parent association, and the
10
+ # corresponding reflection instance. If there is no parent, pass in nil.
11
+ #
12
+ # top = Association.new nil, top_reflection
13
+ # child = Association.new top, child_reflection
14
+ #
15
+ def initialize(parent, reflection)
16
+ @parent, @reflection = parent, reflection
17
+ @children = {}
18
+ end
19
+
20
+ # Get the children associations for a given association name. The only time
21
+ # that there'll actually be more than one association is when the
22
+ # relationship is polymorphic. To keep things simple though, it will always
23
+ # be an Array that gets returned (an empty one if no matches).
24
+ #
25
+ # # where pages is an association on the class tied to the reflection.
26
+ # association.children(:pages)
27
+ #
28
+ def children(assoc)
29
+ @children[assoc] ||= Association.children(@reflection.klass, assoc, self)
30
+ end
31
+
32
+ # Get the children associations for a given class, association name and
33
+ # parent association. Much like the instance method of the same name, it
34
+ # will return an empty array if no associations have the name, and only
35
+ # have multiple association instances if the underlying relationship is
36
+ # polymorphic.
37
+ #
38
+ # Association.children(User, :pages, user_association)
39
+ #
40
+ def self.children(klass, assoc, parent=nil)
41
+ ref = klass.reflect_on_association(assoc)
42
+
43
+ return [] if ref.nil?
44
+ return [Association.new(parent, ref)] unless ref.options[:polymorphic]
45
+
46
+ # association is polymorphic - create associations for each
47
+ # non-polymorphic reflection.
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
+ )
55
+ }
56
+ end
57
+
58
+ # Link up the join for this model from a base join - and set parent
59
+ # associations' joins recursively.
60
+ #
61
+ def join_to(base_join)
62
+ parent.join_to(base_join) if parent && parent.join.nil?
63
+
64
+ @join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new(
65
+ @reflection, base_join, parent ? parent.join : base_join.joins.first
66
+ )
67
+ end
68
+
69
+ # Returns the association's join SQL statements - and it replaces
70
+ # ::ts_join_alias:: with the aliased table name so the generated reflection
71
+ # join conditions avoid column name collisions.
72
+ #
73
+ def to_sql
74
+ @join.association_join.gsub(/::ts_join_alias::/,
75
+ "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
76
+ )
77
+ end
78
+
79
+ # Returns true if the association - or a parent - is a has_many or
80
+ # has_and_belongs_to_many.
81
+ #
82
+ def is_many?
83
+ case @reflection.macro
84
+ when :has_many, :has_and_belongs_to_many
85
+ true
86
+ else
87
+ @parent ? @parent.is_many? : false
88
+ end
89
+ end
90
+
91
+ # Returns an array of all the associations that lead to this one - starting
92
+ # with the top level all the way to the current association object.
93
+ #
94
+ def ancestors
95
+ (parent ? parent.ancestors : []) << self
96
+ end
97
+
98
+ def has_column?(column)
99
+ @reflection.klass.column_names.include?(column.to_s)
100
+ end
101
+
102
+ def primary_key_from_reflection
103
+ if @reflection.options[:through]
104
+ @reflection.source_reflection.options[:foreign_key] ||
105
+ @reflection.source_reflection.primary_key_name
106
+ elsif @reflection.macro == :has_and_belongs_to_many
107
+ @reflection.association_foreign_key
108
+ else
109
+ nil
110
+ end
111
+ end
112
+
113
+ def table
114
+ if @reflection.options[:through] ||
115
+ @reflection.macro == :has_and_belongs_to_many
116
+ @join.aliased_join_table_name
117
+ else
118
+ @join.aliased_table_name
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # Returns all the objects that could be currently instantiated from a
125
+ # polymorphic association. This is pretty damn fast if there's an index on
126
+ # the foreign type column - but if there isn't, it can take a while if you
127
+ # have a lot of data.
128
+ #
129
+ def self.polymorphic_classes(ref)
130
+ ref.active_record.connection.select_all(
131
+ "SELECT DISTINCT #{ref.options[:foreign_type]} " +
132
+ "FROM #{ref.active_record.table_name} " +
133
+ "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
134
+ ).collect { |row|
135
+ row[ref.options[:foreign_type]].constantize
136
+ }
137
+ end
138
+
139
+ # Returns a new set of options for an association that mimics an existing
140
+ # polymorphic relationship for a specific class. It adds a condition to
141
+ # filter by the appropriate object.
142
+ #
143
+ def self.casted_options(klass, ref)
144
+ options = ref.options.clone
145
+ options[:polymorphic] = nil
146
+ options[:class_name] = klass.name
147
+ options[:foreign_key] ||= "#{ref.name}_id"
148
+
149
+ quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
150
+ case options[:conditions]
151
+ when nil
152
+ options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
153
+ when Array
154
+ options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
155
+ when Hash
156
+ options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
157
+ else
158
+ options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
159
+ end
160
+
161
+ options
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,268 @@
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
+ @type = :integer if @type == :string && @crc
79
+
80
+ source.attributes << self
81
+ end
82
+
83
+ # Get the part of the SELECT clause related to this attribute. Don't forget
84
+ # to set your model and associations first though.
85
+ #
86
+ # This will concatenate strings and arrays of integers, and convert
87
+ # datetimes to timestamps, as needed.
88
+ #
89
+ def to_select_sql
90
+ return nil unless include_as_association?
91
+
92
+ separator = all_ints? || @crc ? ',' : ' '
93
+
94
+ clause = @columns.collect { |column|
95
+ part = column_with_prefix(column)
96
+ type == :string ? adapter.convert_nulls(part) : part
97
+ }.join(', ')
98
+
99
+ clause = adapter.cast_to_datetime(clause) if type == :datetime
100
+ clause = adapter.crc(clause) if @crc
101
+ clause = adapter.concatenate(clause, separator) if concat_ws?
102
+ clause = adapter.group_concatenate(clause, separator) if is_many?
103
+
104
+ "#{clause} AS #{quote_column(unique_name)}"
105
+ end
106
+
107
+ def type_to_config
108
+ {
109
+ :multi => :sql_attr_multi,
110
+ :datetime => :sql_attr_timestamp,
111
+ :string => :sql_attr_str2ordinal,
112
+ :float => :sql_attr_float,
113
+ :boolean => :sql_attr_bool,
114
+ :integer => :sql_attr_uint
115
+ }[type]
116
+ end
117
+
118
+ def include_as_association?
119
+ ! (type == :multi && (query_source == :query || query_source == :ranged_query))
120
+ end
121
+
122
+ # Returns the configuration value that should be used for
123
+ # the attribute.
124
+ # Special case is the multi-valued attribute that needs some
125
+ # extra configuration.
126
+ #
127
+ def config_value(offset = nil)
128
+ if type == :multi
129
+ multi_config = include_as_association? ? "field" :
130
+ source_value(offset).gsub(/\n\s*/, " ").strip
131
+ "uint #{unique_name} from #{multi_config}"
132
+ else
133
+ unique_name
134
+ end
135
+ end
136
+
137
+ # Returns the type of the column. If that's not already set, it returns
138
+ # :multi if there's the possibility of more than one value, :string if
139
+ # there's more than one association, otherwise it figures out what the
140
+ # actual column's datatype is and returns that.
141
+ #
142
+ def type
143
+ @type ||= begin
144
+ base_type = case
145
+ when is_many?, is_many_ints?
146
+ :multi
147
+ when @associations.values.flatten.length > 1
148
+ :string
149
+ else
150
+ translated_type_from_database
151
+ end
152
+
153
+ if base_type == :string && @crc
154
+ :integer
155
+ else
156
+ @crc = false
157
+ base_type
158
+ end
159
+ end
160
+ end
161
+
162
+ def updatable?
163
+ [:integer, :datetime, :boolean].include?(type) && !is_string?
164
+ end
165
+
166
+ def live_value(instance)
167
+ object = instance
168
+ column = @columns.first
169
+ column.__stack.each { |method| object = object.send(method) }
170
+ object.send(column.__name)
171
+ end
172
+
173
+ def all_ints?
174
+ @columns.all? { |col|
175
+ klasses = @associations[col].empty? ? [@model] :
176
+ @associations[col].collect { |assoc| assoc.reflection.klass }
177
+ klasses.all? { |klass|
178
+ column = klass.columns.detect { |column| column.name == col.__name.to_s }
179
+ !column.nil? && column.type == :integer
180
+ }
181
+ }
182
+ end
183
+
184
+ private
185
+
186
+ def source_value(offset)
187
+ if is_string?
188
+ "#{query_source.to_s.dasherize}; #{columns.first.__name}"
189
+ elsif query_source == :ranged_query
190
+ "ranged-query; #{query offset} #{query_clause}; #{range_query}"
191
+ else
192
+ "query; #{query offset}"
193
+ end
194
+ end
195
+
196
+ def query(offset)
197
+ assoc = association_for_mva
198
+ raise "Could not determine SQL for MVA" if assoc.nil?
199
+
200
+ <<-SQL
201
+ SELECT #{foreign_key_for_mva assoc}
202
+ #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')},
203
+ #{primary_key_for_mva(assoc)} AS #{quote_column(unique_name)}
204
+ FROM #{quote_table_name assoc.table}
205
+ SQL
206
+ end
207
+
208
+ def query_clause
209
+ foreign_key = foreign_key_for_mva association_for_mva
210
+ "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
211
+ end
212
+
213
+ def range_query
214
+ assoc = association_for_mva
215
+ foreign_key = foreign_key_for_mva assoc
216
+ "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
217
+ end
218
+
219
+ def primary_key_for_mva(assoc)
220
+ quote_with_table(
221
+ assoc.table, assoc.primary_key_from_reflection || columns.first.__name
222
+ )
223
+ end
224
+
225
+ def foreign_key_for_mva(assoc)
226
+ quote_with_table assoc.table, assoc.reflection.primary_key_name
227
+ end
228
+
229
+ def association_for_mva
230
+ @association_for_mva ||= associations[columns.first].detect { |assoc|
231
+ assoc.has_column?(columns.first.__name)
232
+ }
233
+ end
234
+
235
+ def is_many_ints?
236
+ concat_ws? && all_ints?
237
+ end
238
+
239
+ def type_from_database
240
+ klass = @associations.values.flatten.first ?
241
+ @associations.values.flatten.first.reflection.klass : @model
242
+
243
+ klass.columns.detect { |col|
244
+ @columns.collect { |c| c.__name.to_s }.include? col.name
245
+ }.type
246
+ end
247
+
248
+ def translated_type_from_database
249
+ case type_from_db = type_from_database
250
+ when :datetime, :string, :float, :boolean, :integer
251
+ type_from_db
252
+ when :decimal
253
+ :float
254
+ when :timestamp, :date
255
+ :datetime
256
+ else
257
+ raise <<-MESSAGE
258
+
259
+ Cannot automatically map attribute #{unique_name} in #{@model.name} to an
260
+ equivalent Sphinx type (integer, float, boolean, datetime, string as ordinal).
261
+ You could try to explicitly convert the column's value in your define_index
262
+ block:
263
+ has "CAST(column AS INT)", :type => :integer, :as => :column
264
+ MESSAGE
265
+ end
266
+ end
267
+ end
268
+ 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,148 @@
1
+ module ThinkingSphinx
2
+ class Collection < ::Array
3
+ attr_reader :total_entries, :total_pages, :current_page, :per_page
4
+ attr_accessor :results
5
+
6
+ # Compatibility with older versions of will_paginate
7
+ alias_method :page_count, :total_pages
8
+
9
+ def initialize(page, per_page, entries, total_entries)
10
+ @current_page, @per_page, @total_entries = page, per_page, total_entries
11
+
12
+ @total_pages = (entries / @per_page.to_f).ceil
13
+ end
14
+
15
+ def self.ids_from_results(results, page, limit, options)
16
+ collection = self.new(page, limit,
17
+ results[:total] || 0, results[:total_found] || 0
18
+ )
19
+ collection.results = results
20
+ collection.replace results[:matches].collect { |match|
21
+ match[:attributes]["sphinx_internal_id"]
22
+ }
23
+ return collection
24
+ end
25
+
26
+ def self.create_from_results(results, page, limit, options)
27
+ collection = self.new(page, limit,
28
+ results[:total] || 0, results[:total_found] || 0
29
+ )
30
+ collection.results = results
31
+ collection.replace instances_from_matches(results[:matches], options)
32
+ return collection
33
+ end
34
+
35
+ def self.instances_from_matches(matches, options = {})
36
+ if klass = options[:class]
37
+ instances_from_class klass, matches, options
38
+ else
39
+ instances_from_classes matches, options
40
+ end
41
+ end
42
+
43
+ def self.instances_from_class(klass, matches, options = {})
44
+ index_options = klass.sphinx_index_options
45
+
46
+ ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
47
+ instances = ids.length > 0 ? klass.find(
48
+ :all,
49
+ :joins => options[:joins],
50
+ :conditions => {klass.primary_key.to_sym => ids},
51
+ :include => (options[:include] || index_options[:include]),
52
+ :select => (options[:select] || index_options[:select]),
53
+ :order => (options[:sql_order] || index_options[:sql_order])
54
+ ) : []
55
+
56
+ # Raise an exception if we find records in Sphinx but not in the DB, so
57
+ # the search method can retry without them. See
58
+ # ThinkingSphinx::Search.retry_search_on_stale_index.
59
+ if options[:raise_on_stale] && instances.length < ids.length
60
+ stale_ids = ids - instances.map {|i| i.id }
61
+ raise StaleIdsException, stale_ids
62
+ end
63
+
64
+ # if the user has specified an SQL order, return the collection
65
+ # without rearranging it into the Sphinx order
66
+ return instances if options[:sql_order]
67
+
68
+ ids.collect { |obj_id|
69
+ instances.detect { |obj| obj.id == obj_id }
70
+ }
71
+ end
72
+
73
+ # Group results by class and call #find(:all) once for each group to reduce
74
+ # the number of #find's in multi-model searches.
75
+ #
76
+ def self.instances_from_classes(matches, options = {})
77
+ groups = matches.group_by { |match| match[:attributes]["class_crc"] }
78
+ groups.each do |crc, group|
79
+ group.replace(
80
+ instances_from_class(class_from_crc(crc), group, options)
81
+ )
82
+ end
83
+
84
+ matches.collect do |match|
85
+ groups.detect { |crc, group|
86
+ crc == match[:attributes]["class_crc"]
87
+ }[1].detect { |obj|
88
+ obj.id == match[:attributes]["sphinx_internal_id"]
89
+ }
90
+ end
91
+ end
92
+
93
+ def self.class_from_crc(crc)
94
+ @@models_by_crc ||= ThinkingSphinx.indexed_models.inject({}) do |hash, model|
95
+ hash[model.constantize.to_crc32] = model
96
+ model.constantize.subclasses.each { |subclass|
97
+ hash[subclass.to_crc32] = subclass.name
98
+ }
99
+ hash
100
+ end
101
+ @@models_by_crc[crc].constantize
102
+ end
103
+
104
+ def previous_page
105
+ current_page > 1 ? (current_page - 1) : nil
106
+ end
107
+
108
+ def next_page
109
+ current_page < total_pages ? (current_page + 1): nil
110
+ end
111
+
112
+ def offset
113
+ (current_page - 1) * @per_page
114
+ end
115
+
116
+ def method_missing(method, *args, &block)
117
+ super unless method.to_s[/^each_with_.*/]
118
+
119
+ each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
120
+ end
121
+
122
+ def each_with_groupby_and_count(&block)
123
+ results[:matches].each_with_index do |match, index|
124
+ yield self[index], match[:attributes]["@groupby"], match[:attributes]["@count"]
125
+ end
126
+ end
127
+
128
+ def each_with_attribute(attribute, &block)
129
+ results[:matches].each_with_index do |match, index|
130
+ yield self[index], (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
131
+ end
132
+ end
133
+
134
+ def each_with_weighting(&block)
135
+ results[:matches].each_with_index do |match, index|
136
+ yield self[index], match[:weight]
137
+ end
138
+ end
139
+
140
+ def inject_with_groupby_and_count(initial = nil, &block)
141
+ index = -1
142
+ results[:matches].inject(initial) do |memo, match|
143
+ index += 1
144
+ yield memo, self[index], match[:attributes]["@groupby"], match[:attributes]["@count"]
145
+ end
146
+ end
147
+ end
148
+ end