dpickett-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 (79) hide show
  1. data/LICENCE +20 -0
  2. data/README +107 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +74 -0
  4. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  5. data/lib/thinking_sphinx/active_record/search.rb +57 -0
  6. data/lib/thinking_sphinx/active_record.rb +245 -0
  7. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +34 -0
  8. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +53 -0
  9. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +129 -0
  10. data/lib/thinking_sphinx/association.rb +144 -0
  11. data/lib/thinking_sphinx/attribute.rb +254 -0
  12. data/lib/thinking_sphinx/class_facet.rb +20 -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/datetime_delta.rb +50 -0
  17. data/lib/thinking_sphinx/deltas/default_delta.rb +65 -0
  18. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  19. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  20. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  22. data/lib/thinking_sphinx/deltas.rb +22 -0
  23. data/lib/thinking_sphinx/facet.rb +58 -0
  24. data/lib/thinking_sphinx/facet_collection.rb +45 -0
  25. data/lib/thinking_sphinx/field.rb +172 -0
  26. data/lib/thinking_sphinx/index/builder.rb +233 -0
  27. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  28. data/lib/thinking_sphinx/index.rb +432 -0
  29. data/lib/thinking_sphinx/rails_additions.rb +133 -0
  30. data/lib/thinking_sphinx/search.rb +654 -0
  31. data/lib/thinking_sphinx/tasks.rb +128 -0
  32. data/lib/thinking_sphinx.rb +145 -0
  33. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -0
  34. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  35. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  36. data/spec/unit/thinking_sphinx/active_record_spec.rb +256 -0
  37. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  38. data/spec/unit/thinking_sphinx/attribute_spec.rb +212 -0
  39. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  40. data/spec/unit/thinking_sphinx/configuration_spec.rb +136 -0
  41. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  42. data/spec/unit/thinking_sphinx/field_spec.rb +145 -0
  43. data/spec/unit/thinking_sphinx/index/builder_spec.rb +5 -0
  44. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
  45. data/spec/unit/thinking_sphinx/index_spec.rb +54 -0
  46. data/spec/unit/thinking_sphinx/search_spec.rb +59 -0
  47. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  48. data/tasks/distribution.rb +48 -0
  49. data/tasks/rails.rake +1 -0
  50. data/tasks/testing.rb +86 -0
  51. data/vendor/after_commit/LICENSE +20 -0
  52. data/vendor/after_commit/README +16 -0
  53. data/vendor/after_commit/Rakefile +22 -0
  54. data/vendor/after_commit/init.rb +5 -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/lib/after_commit.rb +42 -0
  58. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  59. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  60. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  61. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  62. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  63. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  64. data/vendor/riddle/lib/riddle/client/message.rb +65 -0
  65. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  66. data/vendor/riddle/lib/riddle/client.rb +619 -0
  67. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  68. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  69. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  70. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  71. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  72. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  73. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  74. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  75. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  76. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  77. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  78. data/vendor/riddle/lib/riddle.rb +30 -0
  79. metadata +158 -0
@@ -0,0 +1,129 @@
1
+ module ThinkingSphinx
2
+ class PostgreSQLAdapter < AbstractAdapter
3
+ def setup
4
+ create_array_accum_function
5
+ create_crc32_function
6
+ end
7
+
8
+ def sphinx_identifier
9
+ "pgsql"
10
+ end
11
+
12
+ def concatenate(clause, separator = ' ')
13
+ clause.split(', ').collect { |field|
14
+ "COALESCE(#{field}, '')"
15
+ }.join(" || '#{separator}' || ")
16
+ end
17
+
18
+ def group_concatenate(clause, separator = ' ')
19
+ "array_to_string(array_accum(#{clause}), '#{separator}')"
20
+ end
21
+
22
+ def cast_to_string(clause)
23
+ clause
24
+ end
25
+
26
+ def cast_to_datetime(clause)
27
+ "cast(extract(epoch from #{clause}) as int)"
28
+ end
29
+
30
+ def cast_to_unsigned(clause)
31
+ clause
32
+ end
33
+
34
+ def convert_nulls(clause, default = '')
35
+ default = "'#{default}'" if default.is_a?(String)
36
+
37
+ "COALESCE(#{clause}, #{default})"
38
+ end
39
+
40
+ def boolean(value)
41
+ value ? 'TRUE' : 'FALSE'
42
+ end
43
+
44
+ def crc(clause)
45
+ "crc32(#{clause})"
46
+ end
47
+
48
+ def utf8_query_pre
49
+ nil
50
+ end
51
+
52
+ def time_difference(diff)
53
+ "current_timestamp - interval '#{diff} seconds'"
54
+ end
55
+
56
+ private
57
+
58
+ def execute(command, output_error = false)
59
+ connection.execute "begin"
60
+ connection.execute "savepoint ts"
61
+ begin
62
+ connection.execute command
63
+ rescue StandardError => err
64
+ puts err if output_error
65
+ connection.execute "rollback to savepoint ts"
66
+ end
67
+ connection.execute "release savepoint ts"
68
+ connection.execute "commit"
69
+ end
70
+
71
+ def create_array_accum_function
72
+ if connection.raw_connection.server_version > 80200
73
+ execute <<-SQL
74
+ CREATE AGGREGATE array_accum (anyelement)
75
+ (
76
+ sfunc = array_append,
77
+ stype = anyarray,
78
+ initcond = '{}'
79
+ );
80
+ SQL
81
+ else
82
+ execute <<-SQL
83
+ CREATE AGGREGATE array_accum
84
+ (
85
+ basetype = anyelement,
86
+ sfunc = array_append,
87
+ stype = anyarray,
88
+ initcond = '{}'
89
+ );
90
+ SQL
91
+ end
92
+ end
93
+
94
+ def create_crc32_function
95
+ execute "CREATE LANGUAGE 'plpgsql';"
96
+ function = <<-SQL
97
+ CREATE OR REPLACE FUNCTION crc32(word text)
98
+ RETURNS bigint AS $$
99
+ DECLARE tmp bigint;
100
+ DECLARE i int;
101
+ DECLARE j int;
102
+ DECLARE word_array bytea;
103
+ BEGIN
104
+ i = 0;
105
+ tmp = 4294967295;
106
+ word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
107
+ LOOP
108
+ tmp = (tmp # get_byte(word_array, i))::bigint;
109
+ i = i + 1;
110
+ j = 0;
111
+ LOOP
112
+ tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
113
+ j = j + 1;
114
+ IF j >= 8 THEN
115
+ EXIT;
116
+ END IF;
117
+ END LOOP;
118
+ IF i >= char_length(word) THEN
119
+ EXIT;
120
+ END IF;
121
+ END LOOP;
122
+ return (tmp # 4294967295);
123
+ END
124
+ $$ IMMUTABLE STRICT LANGUAGE plpgsql;
125
+ SQL
126
+ execute function, true
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,144 @@
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
+ private
103
+
104
+ # Returns all the objects that could be currently instantiated from a
105
+ # polymorphic association. This is pretty damn fast if there's an index on
106
+ # the foreign type column - but if there isn't, it can take a while if you
107
+ # have a lot of data.
108
+ #
109
+ def self.polymorphic_classes(ref)
110
+ ref.active_record.connection.select_all(
111
+ "SELECT DISTINCT #{ref.options[:foreign_type]} " +
112
+ "FROM #{ref.active_record.table_name} " +
113
+ "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
114
+ ).collect { |row|
115
+ row[ref.options[:foreign_type]].constantize
116
+ }
117
+ end
118
+
119
+ # Returns a new set of options for an association that mimics an existing
120
+ # polymorphic relationship for a specific class. It adds a condition to
121
+ # filter by the appropriate object.
122
+ #
123
+ def self.casted_options(klass, ref)
124
+ options = ref.options.clone
125
+ options[:polymorphic] = nil
126
+ options[:class_name] = klass.name
127
+ options[:foreign_key] ||= "#{ref.name}_id"
128
+
129
+ quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
130
+ case options[:conditions]
131
+ when nil
132
+ options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
133
+ when Array
134
+ options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
135
+ when Hash
136
+ options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
137
+ else
138
+ options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
139
+ end
140
+
141
+ options
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,254 @@
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
12
+ attr_accessor :alias, :columns, :associations, :model, :faceted
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
+ #
21
+ # Alias is only required in three circumstances: when there's
22
+ # another attribute or field with the same name, when the column name is
23
+ # 'id', or when there's more than one column.
24
+ #
25
+ # Type is not required, unless you want to force a column to be a certain
26
+ # type (but keep in mind the value will not be CASTed in the SQL
27
+ # statements). The only time you really need to use this is when the type
28
+ # can't be figured out by the column - ie: when not actually using a
29
+ # database column as your source.
30
+ #
31
+ # Example usage:
32
+ #
33
+ # Attribute.new(
34
+ # Column.new(:created_at)
35
+ # )
36
+ #
37
+ # Attribute.new(
38
+ # Column.new(:posts, :id),
39
+ # :as => :post_ids
40
+ # )
41
+ #
42
+ # Attribute.new(
43
+ # [Column.new(:pages, :id), Column.new(:articles, :id)],
44
+ # :as => :content_ids
45
+ # )
46
+ #
47
+ # Attribute.new(
48
+ # Column.new("NOW()"),
49
+ # :as => :indexed_at,
50
+ # :type => :datetime
51
+ # )
52
+ #
53
+ # If you're creating attributes for latitude and longitude, don't forget
54
+ # that Sphinx expects these values to be in radians.
55
+ #
56
+ def initialize(columns, options = {})
57
+ @columns = Array(columns)
58
+ @associations = {}
59
+
60
+ raise "Cannot define a field with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }
61
+
62
+ @alias = options[:as]
63
+ @type = options[:type]
64
+ @faceted = options[:facet]
65
+ end
66
+
67
+ # Get the part of the SELECT clause related to this attribute. Don't forget
68
+ # to set your model and associations first though.
69
+ #
70
+ # This will concatenate strings and arrays of integers, and convert
71
+ # datetimes to timestamps, as needed.
72
+ #
73
+ def to_select_sql
74
+ clause = @columns.collect { |column|
75
+ column_with_prefix(column)
76
+ }.join(', ')
77
+
78
+ separator = all_ints? ? ',' : ' '
79
+
80
+ clause = adapter.concatenate(clause, separator) if concat_ws?
81
+ clause = adapter.group_concatenate(clause, separator) if is_many?
82
+ clause = adapter.cast_to_datetime(clause) if type == :datetime
83
+ clause = adapter.convert_nulls(clause) if type == :string
84
+
85
+ "#{clause} AS #{quote_column(unique_name)}"
86
+ end
87
+
88
+ # Get the part of the GROUP BY clause related to this attribute - if one is
89
+ # needed. If not, all you'll get back is nil. The latter will happen if
90
+ # there isn't actually a real column to get data from, or if there's
91
+ # multiple data values (read: a has_many or has_and_belongs_to_many
92
+ # association).
93
+ #
94
+ def to_group_sql
95
+ case
96
+ when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
97
+ nil
98
+ else
99
+ @columns.collect { |column|
100
+ column_with_prefix(column)
101
+ }
102
+ end
103
+ end
104
+
105
+ def type_to_config
106
+ {
107
+ :multi => :sql_attr_multi,
108
+ :datetime => :sql_attr_timestamp,
109
+ :string => :sql_attr_str2ordinal,
110
+ :float => :sql_attr_float,
111
+ :boolean => :sql_attr_bool,
112
+ :integer => :sql_attr_uint
113
+ }[type]
114
+ end
115
+
116
+ def config_value
117
+ if type == :multi
118
+ "uint #{unique_name} from field"
119
+ else
120
+ unique_name
121
+ end
122
+ end
123
+
124
+ # Returns the unique name of the attribute - which is either the alias of
125
+ # the attribute, or the name of the only column - if there is only one. If
126
+ # there isn't, there should be an alias. Else things probably won't work.
127
+ # Consider yourself warned.
128
+ #
129
+ def unique_name
130
+ if @columns.length == 1
131
+ @alias || @columns.first.__name
132
+ else
133
+ @alias
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
+ def type
142
+ @type ||= case
143
+ when is_many?
144
+ :multi
145
+ when @associations.values.flatten.length > 1
146
+ :string
147
+ else
148
+ translated_type_from_database
149
+ end
150
+ end
151
+
152
+ def to_facet
153
+ return nil unless @faceted
154
+
155
+ ThinkingSphinx::Facet.new(self)
156
+ end
157
+
158
+ private
159
+
160
+ def adapter
161
+ @adapter ||= @model.sphinx_database_adapter
162
+ end
163
+
164
+ def quote_column(column)
165
+ @model.connection.quote_column_name(column)
166
+ end
167
+
168
+ # Indication of whether the columns should be concatenated with a space
169
+ # between each value. True if there's either multiple sources or multiple
170
+ # associations.
171
+ #
172
+ def concat_ws?
173
+ multiple_associations? || @columns.length > 1
174
+ end
175
+
176
+ # Checks whether any column requires multiple associations (which only
177
+ # happens for polymorphic situations).
178
+ #
179
+ def multiple_associations?
180
+ associations.any? { |col,assocs| assocs.length > 1 }
181
+ end
182
+
183
+ # Builds a column reference tied to the appropriate associations. This
184
+ # dives into the associations hash and their corresponding joins to
185
+ # figure out how to correctly reference a column in SQL.
186
+ #
187
+ def column_with_prefix(column)
188
+ if column.is_string?
189
+ column.__name
190
+ elsif associations[column].empty?
191
+ "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
192
+ else
193
+ associations[column].collect { |assoc|
194
+ assoc.has_column?(column.__name) ?
195
+ "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
196
+ ".#{quote_column(column.__name)}" :
197
+ nil
198
+ }.compact.join(', ')
199
+ end
200
+ end
201
+
202
+ # Could there be more than one value related to the parent record? If so,
203
+ # then this will return true. If not, false. It's that simple.
204
+ #
205
+ def is_many?
206
+ associations.values.flatten.any? { |assoc| assoc.is_many? }
207
+ end
208
+
209
+ # Returns true if any of the columns are string values, instead of database
210
+ # column references.
211
+ def is_string?
212
+ columns.all? { |col| col.is_string? }
213
+ end
214
+
215
+ def all_ints?
216
+ @columns.all? { |col|
217
+ klasses = @associations[col].empty? ? [@model] :
218
+ @associations[col].collect { |assoc| assoc.reflection.klass }
219
+ klasses.all? { |klass|
220
+ column = klass.columns.detect { |column| column.name == col.__name.to_s }
221
+ !column.nil? && column.type == :integer
222
+ }
223
+ }
224
+ end
225
+
226
+ def type_from_database
227
+ klass = @associations.values.flatten.first ?
228
+ @associations.values.flatten.first.reflection.klass : @model
229
+
230
+ klass.columns.detect { |col|
231
+ @columns.collect { |c| c.__name.to_s }.include? col.name
232
+ }.type
233
+ end
234
+
235
+ def translated_type_from_database
236
+ case type_from_db = type_from_database
237
+ when :datetime, :string, :float, :boolean, :integer
238
+ type_from_db
239
+ when :decimal
240
+ :float
241
+ when :timestamp, :date
242
+ :datetime
243
+ else
244
+ raise <<-MESSAGE
245
+
246
+ Cannot automatically map column type #{type_from_db} to an equivalent Sphinx
247
+ type (integer, float, boolean, datetime, string as ordinal). You could try to
248
+ explicitly convert the column's value in your define_index block:
249
+ has "CAST(column AS INT)", :type => :integer, :as => :column
250
+ MESSAGE
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,20 @@
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
+ # ThinkingSphinx.indexed_models.each do |i|
14
+ # return i if i.to_crc32 == attribute_value
15
+ # end
16
+ #
17
+ # raise "Unknown class crc"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,142 @@
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
+ :conditions => {klass.primary_key.to_sym => ids},
50
+ :include => (options[:include] || index_options[:include]),
51
+ :select => (options[:select] || index_options[:select])
52
+ ) : []
53
+
54
+ # Raise an exception if we find records in Sphinx but not in the DB, so
55
+ # the search method can retry without them. See
56
+ # ThinkingSphinx::Search.retry_search_on_stale_index.
57
+ if options[:raise_on_stale] && instances.length < ids.length
58
+ stale_ids = ids - instances.map {|i| i.id }
59
+ raise StaleIdsException, stale_ids
60
+ end
61
+
62
+ ids.collect { |obj_id|
63
+ instances.detect { |obj| obj.id == obj_id }
64
+ }
65
+ end
66
+
67
+ # Group results by class and call #find(:all) once for each group to reduce
68
+ # the number of #find's in multi-model searches.
69
+ #
70
+ def self.instances_from_classes(matches, options = {})
71
+ groups = matches.group_by { |match| match[:attributes]["class_crc"] }
72
+ groups.each do |crc, group|
73
+ group.replace(
74
+ instances_from_class(class_from_crc(crc), group, options)
75
+ )
76
+ end
77
+
78
+ matches.collect do |match|
79
+ groups.detect { |crc, group|
80
+ crc == match[:attributes]["class_crc"]
81
+ }[1].detect { |obj|
82
+ obj.id == match[:attributes]["sphinx_internal_id"]
83
+ }
84
+ end
85
+ end
86
+
87
+ def self.class_from_crc(crc)
88
+ @@models_by_crc ||= ThinkingSphinx.indexed_models.inject({}) do |hash, model|
89
+ hash[model.constantize.to_crc32] = model
90
+ model.constantize.subclasses.each { |subclass|
91
+ hash[subclass.to_crc32] = subclass.name
92
+ }
93
+ hash
94
+ end
95
+ @@models_by_crc[crc].constantize
96
+ end
97
+
98
+ def previous_page
99
+ current_page > 1 ? (current_page - 1) : nil
100
+ end
101
+
102
+ def next_page
103
+ current_page < total_pages ? (current_page + 1): nil
104
+ end
105
+
106
+ def offset
107
+ (current_page - 1) * @per_page
108
+ end
109
+
110
+ def method_missing(method, *args, &block)
111
+ super unless method.to_s[/^each_with_.*/]
112
+
113
+ each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
114
+ end
115
+
116
+ def each_with_groupby_and_count(&block)
117
+ results[:matches].each_with_index do |match, index|
118
+ yield self[index], match[:attributes]["@groupby"], match[:attributes]["@count"]
119
+ end
120
+ end
121
+
122
+ def each_with_attribute(attribute, &block)
123
+ results[:matches].each_with_index do |match, index|
124
+ yield self[index], (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
125
+ end
126
+ end
127
+
128
+ def each_with_weighting(&block)
129
+ results[:matches].each_with_index do |match, index|
130
+ yield self[index], match[:weight]
131
+ end
132
+ end
133
+
134
+ def inject_with_groupby_and_count(initial = nil, &block)
135
+ index = -1
136
+ results[:matches].inject(initial) do |memo, match|
137
+ index += 1
138
+ yield memo, self[index], match[:attributes]["@groupby"], match[:attributes]["@count"]
139
+ end
140
+ end
141
+ end
142
+ end