dpickett-thinking-sphinx 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
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