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,110 @@
1
+ module ThinkingSphinx
2
+ class Index
3
+ # Instances of this class represent database columns and the stack of
4
+ # associations that lead from the base model to them.
5
+ #
6
+ # The name and stack are accessible through methods starting with __ to
7
+ # avoid conflicting with the method_missing calls that build the stack.
8
+ #
9
+ class FauxColumn
10
+ # Create a new column with a pre-defined stack. The top element in the
11
+ # stack will get shifted to be the name value.
12
+ #
13
+ def initialize(*stack)
14
+ @name = stack.pop
15
+ @stack = stack
16
+ end
17
+
18
+ def self.coerce(columns)
19
+ case columns
20
+ when Symbol, String
21
+ FauxColumn.new(columns)
22
+ when Array
23
+ columns.collect { |col| FauxColumn.coerce(col) }
24
+ when FauxColumn
25
+ columns
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ # Can't use normal method name, as that could be an association or
32
+ # column name.
33
+ #
34
+ def __name
35
+ @name
36
+ end
37
+
38
+ # Can't use normal method name, as that could be an association or
39
+ # column name.
40
+ #
41
+ def __stack
42
+ @stack
43
+ end
44
+
45
+ # Returns true if the stack is empty *and* if the name is a string -
46
+ # which is an indication that of raw SQL, as opposed to a value from a
47
+ # table's column.
48
+ #
49
+ def is_string?
50
+ @name.is_a?(String) && @stack.empty?
51
+ end
52
+
53
+ # This handles any 'invalid' method calls and sets them as the name,
54
+ # and pushing the previous name into the stack. The object returns
55
+ # itself.
56
+ #
57
+ # If there's a single argument, it becomes the name, and the method
58
+ # symbol goes into the stack as well. Multiple arguments means new
59
+ # columns with the original stack and new names (from each argument) gets
60
+ # returned.
61
+ #
62
+ # Easier to explain with examples:
63
+ #
64
+ # col = FauxColumn.new :a, :b, :c
65
+ # col.__name #=> :c
66
+ # col.__stack #=> [:a, :b]
67
+ #
68
+ # col.whatever #=> col
69
+ # col.__name #=> :whatever
70
+ # col.__stack #=> [:a, :b, :c]
71
+ #
72
+ # col.something(:id) #=> col
73
+ # col.__name #=> :id
74
+ # col.__stack #=> [:a, :b, :c, :whatever, :something]
75
+ #
76
+ # cols = col.short(:x, :y, :z)
77
+ # cols[0].__name #=> :x
78
+ # cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short]
79
+ # cols[1].__name #=> :y
80
+ # cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short]
81
+ # cols[2].__name #=> :z
82
+ # cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short]
83
+ #
84
+ # Also, this allows method chaining to build up a relevant stack:
85
+ #
86
+ # col = FauxColumn.new :a, :b
87
+ # col.__name #=> :b
88
+ # col.__stack #=> [:a]
89
+ #
90
+ # col.one.two.three #=> col
91
+ # col.__name #=> :three
92
+ # col.__stack #=> [:a, :b, :one, :two]
93
+ #
94
+ def method_missing(method, *args)
95
+ @stack << @name
96
+ @name = method
97
+
98
+ if (args.empty?)
99
+ self
100
+ elsif (args.length == 1)
101
+ method_missing(args.first)
102
+ else
103
+ args.collect { |arg|
104
+ FauxColumn.new(@stack + [@name, arg])
105
+ }
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,163 @@
1
+ module ThinkingSphinx
2
+ class Property
3
+ attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
4
+
5
+ def initialize(source, columns, options = {})
6
+ @source = source
7
+ @model = source.model
8
+ @columns = Array(columns)
9
+ @associations = {}
10
+
11
+ raise "Cannot define a field or attribute in #{source.model.name} 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) }
12
+
13
+ @alias = options[:as]
14
+ @faceted = options[:facet]
15
+ @admin = options[:admin]
16
+
17
+ @alias = @alias.to_sym unless @alias.blank?
18
+
19
+ @columns.each { |col|
20
+ @associations[col] = association_stack(col.__stack.clone).each { |assoc|
21
+ assoc.join_to(source.base)
22
+ assoc.columns << col
23
+ }
24
+ }
25
+ end
26
+
27
+ # Returns the unique name of the attribute - which is either the alias of
28
+ # the attribute, or the name of the only column - if there is only one. If
29
+ # there isn't, there should be an alias. Else things probably won't work.
30
+ # Consider yourself warned.
31
+ #
32
+ def unique_name
33
+ if @columns.length == 1
34
+ @alias || @columns.first.__name
35
+ else
36
+ @alias
37
+ end
38
+ end
39
+
40
+ def to_facet
41
+ return nil unless @faceted
42
+
43
+ ThinkingSphinx::Facet.new(self)
44
+ end
45
+
46
+ # Get the part of the GROUP BY clause related to this attribute - if one is
47
+ # needed. If not, all you'll get back is nil. The latter will happen if
48
+ # there isn't actually a real column to get data from, or if there's
49
+ # multiple data values (read: a has_many or has_and_belongs_to_many
50
+ # association).
51
+ #
52
+ def to_group_sql
53
+ case
54
+ when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
55
+ nil
56
+ else
57
+ @columns.collect { |column|
58
+ column_with_prefix(column)
59
+ }
60
+ end
61
+ end
62
+
63
+ def changed?(instance)
64
+ return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
65
+
66
+ !@columns.all? { |col|
67
+ instance.respond_to?("#{col.__name.to_s}_changed?") &&
68
+ !instance.send("#{col.__name.to_s}_changed?")
69
+ }
70
+ end
71
+
72
+ def admin?
73
+ admin
74
+ end
75
+
76
+ def public?
77
+ !admin
78
+ end
79
+
80
+ private
81
+
82
+ # Could there be more than one value related to the parent record? If so,
83
+ # then this will return true. If not, false. It's that simple.
84
+ #
85
+ def is_many?
86
+ associations.values.flatten.any? { |assoc| assoc.is_many? }
87
+ end
88
+
89
+ # Returns true if any of the columns are string values, instead of database
90
+ # column references.
91
+ def is_string?
92
+ columns.all? { |col| col.is_string? }
93
+ end
94
+
95
+ def adapter
96
+ @adapter ||= @model.sphinx_database_adapter
97
+ end
98
+
99
+ def quote_with_table(table, column)
100
+ "#{quote_table_name(table)}.#{quote_column(column)}"
101
+ end
102
+
103
+ def quote_column(column)
104
+ @model.connection.quote_column_name(column)
105
+ end
106
+
107
+ def quote_table_name(table_name)
108
+ @model.connection.quote_table_name(table_name)
109
+ end
110
+
111
+ # Indication of whether the columns should be concatenated with a space
112
+ # between each value. True if there's either multiple sources or multiple
113
+ # associations.
114
+ #
115
+ def concat_ws?
116
+ multiple_associations? || @columns.length > 1
117
+ end
118
+
119
+ # Checks whether any column requires multiple associations (which only
120
+ # happens for polymorphic situations).
121
+ #
122
+ def multiple_associations?
123
+ associations.any? { |col,assocs| assocs.length > 1 }
124
+ end
125
+
126
+ # Builds a column reference tied to the appropriate associations. This
127
+ # dives into the associations hash and their corresponding joins to
128
+ # figure out how to correctly reference a column in SQL.
129
+ #
130
+ def column_with_prefix(column)
131
+ if column.is_string?
132
+ column.__name
133
+ elsif associations[column].empty?
134
+ "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
135
+ else
136
+ associations[column].collect { |assoc|
137
+ assoc.has_column?(column.__name) ?
138
+ "#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
139
+ nil
140
+ }.compact.join(', ')
141
+ end
142
+ end
143
+
144
+ # Gets a stack of associations for a specific path.
145
+ #
146
+ def association_stack(path, parent = nil)
147
+ assocs = []
148
+
149
+ if parent.nil?
150
+ assocs = @source.association(path.shift)
151
+ else
152
+ assocs = parent.children(path.shift)
153
+ end
154
+
155
+ until path.empty?
156
+ point = path.shift
157
+ assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
158
+ end
159
+
160
+ assocs
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,150 @@
1
+ module ThinkingSphinx
2
+ module HashExcept
3
+ # Returns a new hash without the given keys.
4
+ def except(*keys)
5
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
6
+ reject { |key,| rejected.include?(key) }
7
+ end
8
+
9
+ # Replaces the hash without only the given keys.
10
+ def except!(*keys)
11
+ replace(except(*keys))
12
+ end
13
+ end
14
+ end
15
+
16
+ Hash.send(
17
+ :include, ThinkingSphinx::HashExcept
18
+ ) unless Hash.instance_methods.include?("except")
19
+
20
+ module ThinkingSphinx
21
+ module ArrayExtractOptions
22
+ def extract_options!
23
+ last.is_a?(::Hash) ? pop : {}
24
+ end
25
+ end
26
+ end
27
+
28
+ Array.send(
29
+ :include, ThinkingSphinx::ArrayExtractOptions
30
+ ) unless Array.instance_methods.include?("extract_options!")
31
+
32
+ module ThinkingSphinx
33
+ module AbstractQuotedTableName
34
+ def quote_table_name(name)
35
+ quote_column_name(name)
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
41
+ :include, ThinkingSphinx::AbstractQuotedTableName
42
+ ) unless ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_methods.include?("quote_table_name")
43
+
44
+ module ThinkingSphinx
45
+ module MysqlQuotedTableName
46
+ def quote_table_name(name) #:nodoc:
47
+ quote_column_name(name).gsub('.', '`.`')
48
+ end
49
+ end
50
+ end
51
+
52
+ if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") or ActiveRecord::Base.respond_to?(:jdbcmysql_connection)
53
+ adapter = ActiveRecord::ConnectionAdapters.const_get(
54
+ defined?(JRUBY_VERSION) ? :JdbcAdapter : :MysqlAdapter
55
+ )
56
+ unless adapter.instance_methods.include?("quote_table_name")
57
+ adapter.send(:include, ThinkingSphinx::MysqlQuotedTableName)
58
+ end
59
+ end
60
+
61
+ module ThinkingSphinx
62
+ module ActiveRecordQuotedName
63
+ def quoted_table_name
64
+ self.connection.quote_table_name(self.table_name)
65
+ end
66
+ end
67
+ end
68
+
69
+ ActiveRecord::Base.extend(
70
+ ThinkingSphinx::ActiveRecordQuotedName
71
+ ) unless ActiveRecord::Base.respond_to?("quoted_table_name")
72
+
73
+ module ThinkingSphinx
74
+ module ActiveRecordStoreFullSTIClass
75
+ def store_full_sti_class
76
+ false
77
+ end
78
+ end
79
+ end
80
+
81
+ ActiveRecord::Base.extend(
82
+ ThinkingSphinx::ActiveRecordStoreFullSTIClass
83
+ ) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
84
+
85
+ module ThinkingSphinx
86
+ module ClassAttributeMethods
87
+ def cattr_reader(*syms)
88
+ syms.flatten.each do |sym|
89
+ next if sym.is_a?(Hash)
90
+ class_eval(<<-EOS, __FILE__, __LINE__)
91
+ unless defined? @@#{sym}
92
+ @@#{sym} = nil
93
+ end
94
+
95
+ def self.#{sym}
96
+ @@#{sym}
97
+ end
98
+
99
+ def #{sym}
100
+ @@#{sym}
101
+ end
102
+ EOS
103
+ end
104
+ end
105
+
106
+ def cattr_writer(*syms)
107
+ options = syms.extract_options!
108
+ syms.flatten.each do |sym|
109
+ class_eval(<<-EOS, __FILE__, __LINE__)
110
+ unless defined? @@#{sym}
111
+ @@#{sym} = nil
112
+ end
113
+
114
+ def self.#{sym}=(obj)
115
+ @@#{sym} = obj
116
+ end
117
+
118
+ #{"
119
+ def #{sym}=(obj)
120
+ @@#{sym} = obj
121
+ end
122
+ " unless options[:instance_writer] == false }
123
+ EOS
124
+ end
125
+ end
126
+
127
+ def cattr_accessor(*syms)
128
+ cattr_reader(*syms)
129
+ cattr_writer(*syms)
130
+ end
131
+ end
132
+ end
133
+
134
+ Class.extend(
135
+ ThinkingSphinx::ClassAttributeMethods
136
+ ) unless Class.respond_to?(:cattr_reader)
137
+
138
+ module ThinkingSphinx
139
+ module MetaClass
140
+ def metaclass
141
+ class << self
142
+ self
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ unless Object.new.respond_to?(:metaclass)
149
+ Object.send(:include, ThinkingSphinx::MetaClass)
150
+ end
@@ -0,0 +1,708 @@
1
+ # encoding: UTF-8
2
+ module ThinkingSphinx
3
+ # Once you've got those indexes in and built, this is the stuff that
4
+ # matters - how to search! This class provides a generic search
5
+ # interface - which you can use to search all your indexed models at once.
6
+ # Most times, you will just want a specific model's results - to search and
7
+ # search_for_ids methods will do the job in exactly the same manner when
8
+ # called from a model.
9
+ #
10
+ class Search
11
+ CoreMethods = %w( == class class_eval extend frozen? id instance_eval
12
+ instance_of? instance_values instance_variable_defined?
13
+ instance_variable_get instance_variable_set instance_variables is_a?
14
+ kind_of? member? method methods nil? object_id respond_to? send should
15
+ type )
16
+ SafeMethods = %w( partition private_methods protected_methods
17
+ public_methods send )
18
+
19
+ instance_methods.select { |method|
20
+ method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
21
+ }.each { |method|
22
+ undef_method method
23
+ }
24
+
25
+ HashOptions = [:conditions, :with, :without, :with_all]
26
+ ArrayOptions = [:classes, :without_ids]
27
+
28
+ attr_reader :args, :options
29
+
30
+ # Deprecated. Use ThinkingSphinx.search
31
+ def self.search(*args)
32
+ log 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
33
+ ThinkingSphinx.search *args
34
+ end
35
+
36
+ # Deprecated. Use ThinkingSphinx.search_for_ids
37
+ def self.search_for_ids(*args)
38
+ log 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
39
+ ThinkingSphinx.search_for_ids *args
40
+ end
41
+
42
+ # Deprecated. Use ThinkingSphinx.search_for_ids
43
+ def self.search_for_id(*args)
44
+ log 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
45
+ ThinkingSphinx.search_for_id *args
46
+ end
47
+
48
+ # Deprecated. Use ThinkingSphinx.count
49
+ def self.count(*args)
50
+ log 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
51
+ ThinkingSphinx.count *args
52
+ end
53
+
54
+ # Deprecated. Use ThinkingSphinx.facets
55
+ def self.facets(*args)
56
+ log 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
57
+ ThinkingSphinx.facets *args
58
+ end
59
+
60
+ def initialize(*args)
61
+ @array = []
62
+ @options = args.extract_options!
63
+ @args = args
64
+ end
65
+
66
+ def to_a
67
+ populate
68
+ @array
69
+ end
70
+
71
+ # Indication of whether the request has been made to Sphinx for the search
72
+ # query.
73
+ #
74
+ # @return [Boolean] true if the results have been requested.
75
+ #
76
+ def populated?
77
+ !!@populated
78
+ end
79
+
80
+ # The query result hash from Riddle.
81
+ #
82
+ # @return [Hash] Raw Sphinx results
83
+ #
84
+ def results
85
+ populate
86
+ @results
87
+ end
88
+
89
+ def method_missing(method, *args, &block)
90
+ if is_scope?(method)
91
+ add_scope(method, *args, &block)
92
+ return self
93
+ elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
94
+ super
95
+ elsif !SafeMethods.include?(method.to_s)
96
+ populate
97
+ end
98
+
99
+ if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
100
+ each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
101
+ else
102
+ @array.send(method, *args, &block)
103
+ end
104
+ end
105
+
106
+ # Returns true if the Search object or the underlying Array object respond
107
+ # to the requested method.
108
+ #
109
+ # @param [Symbol] method The method name
110
+ # @return [Boolean] true if either Search or Array responds to the method.
111
+ #
112
+ def respond_to?(method)
113
+ super || @array.respond_to?(method)
114
+ end
115
+
116
+ # The current page number of the result set. Defaults to 1 if no page was
117
+ # explicitly requested.
118
+ #
119
+ # @return [Integer]
120
+ #
121
+ def current_page
122
+ @options[:page].blank? ? 1 : @options[:page].to_i
123
+ end
124
+
125
+ # The next page number of the result set. If there are no more pages
126
+ # available, nil is returned.
127
+ #
128
+ # @return [Integer, nil]
129
+ #
130
+ def next_page
131
+ current_page >= total_pages ? nil : current_page + 1
132
+ end
133
+
134
+ # The previous page number of the result set. If this is the first page,
135
+ # then nil is returned.
136
+ #
137
+ # @return [Integer, nil]
138
+ #
139
+ def previous_page
140
+ current_page == 1 ? nil : current_page - 1
141
+ end
142
+
143
+ # The amount of records per set of paged results. Defaults to 20 unless a
144
+ # specific page size is requested.
145
+ #
146
+ # @return [Integer]
147
+ #
148
+ def per_page
149
+ @options[:limit] || @options[:per_page] || 20
150
+ end
151
+
152
+ # The total number of pages available if the results are paginated.
153
+ #
154
+ # @return [Integer]
155
+ #
156
+ def total_pages
157
+ populate
158
+ @total_pages ||= (@results[:total] / per_page.to_f).ceil
159
+ end
160
+ # Compatibility with older versions of will_paginate
161
+ alias_method :page_count, :total_pages
162
+
163
+ # The total number of search results available.
164
+ #
165
+ # @return [Integer]
166
+ #
167
+ def total_entries
168
+ populate
169
+ @total_entries ||= @results[:total_found]
170
+ end
171
+
172
+ # The current page's offset, based on the number of records per page.
173
+ #
174
+ # @return [Integer]
175
+ #
176
+ def offset
177
+ (current_page - 1) * per_page
178
+ end
179
+
180
+ def indexes
181
+ return options[:index] if options[:index]
182
+ return '*' if classes.empty?
183
+
184
+ classes.collect { |klass| klass.sphinx_index_names }.flatten.join(',')
185
+ end
186
+
187
+ def each_with_groupby_and_count(&block)
188
+ populate
189
+ results[:matches].each_with_index do |match, index|
190
+ yield self[index],
191
+ match[:attributes]["@groupby"],
192
+ match[:attributes]["@count"]
193
+ end
194
+ end
195
+ alias_method :each_with_group_and_count, :each_with_groupby_and_count
196
+
197
+ def each_with_weighting(&block)
198
+ populate
199
+ results[:matches].each_with_index do |match, index|
200
+ yield self[index], match[:weight]
201
+ end
202
+ end
203
+
204
+ def excerpt_for(string, model = nil)
205
+ if model.nil? && one_class
206
+ model ||= one_class
207
+ end
208
+
209
+ populate
210
+ client.excerpts(
211
+ :docs => [string],
212
+ :words => results[:words].keys.join(' '),
213
+ :index => "#{model.source_of_sphinx_index.sphinx_name}_core"
214
+ ).first
215
+ end
216
+
217
+ def search(*args)
218
+ merge_search ThinkingSphinx::Search.new(*args)
219
+ self
220
+ end
221
+
222
+ private
223
+
224
+ def config
225
+ ThinkingSphinx::Configuration.instance
226
+ end
227
+
228
+ def populate
229
+ return if @populated
230
+ @populated = true
231
+
232
+ retry_on_stale_index do
233
+ begin
234
+ log "Querying Sphinx: #{query}"
235
+ @results = client.query query, indexes, comment
236
+ rescue Errno::ECONNREFUSED => err
237
+ raise ThinkingSphinx::ConnectionError,
238
+ 'Connection to Sphinx Daemon (searchd) failed.'
239
+ end
240
+
241
+ if options[:ids_only]
242
+ replace @results[:matches].collect { |match|
243
+ match[:attributes]["sphinx_internal_id"]
244
+ }
245
+ else
246
+ replace instances_from_matches
247
+ add_excerpter
248
+ add_sphinx_attributes
249
+ end
250
+ end
251
+ end
252
+
253
+ def add_excerpter
254
+ each do |object|
255
+ next if object.respond_to?(:excerpts)
256
+
257
+ excerpter = ThinkingSphinx::Excerpter.new self, object
258
+ block = lambda { excerpter }
259
+
260
+ object.metaclass.instance_eval do
261
+ define_method(:excerpts, &block)
262
+ end
263
+ end
264
+ end
265
+
266
+ def add_sphinx_attributes
267
+ each do |object|
268
+ next if object.nil? || object.respond_to?(:sphinx_attributes)
269
+
270
+ match = @results[:matches].detect { |match|
271
+ match[:attributes]['sphinx_internal_id'] == object.
272
+ primary_key_for_sphinx &&
273
+ match[:attributes]['class_crc'] == object.class.to_crc32
274
+ }
275
+ next if match.nil?
276
+
277
+ object.metaclass.instance_eval do
278
+ define_method(:sphinx_attributes) { match[:attributes] }
279
+ end
280
+ end
281
+ end
282
+
283
+ def self.log(message, method = :debug)
284
+ return if ::ActiveRecord::Base.logger.nil?
285
+ ::ActiveRecord::Base.logger.send method, message
286
+ end
287
+
288
+ def log(message, method = :debug)
289
+ self.class.log(message, method)
290
+ end
291
+
292
+ def client
293
+ client = config.client
294
+
295
+ index_options = one_class ?
296
+ one_class.sphinx_indexes.first.local_options : {}
297
+
298
+ [
299
+ :max_matches, :group_by, :group_function, :group_clause,
300
+ :group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
301
+ :rank_mode, :max_query_time, :field_weights
302
+ ].each do |key|
303
+ # puts "key: #{key}"
304
+ value = options[key] || index_options[key]
305
+ # puts "value: #{value.inspect}"
306
+ client.send("#{key}=", value) if value
307
+ end
308
+
309
+ client.limit = per_page
310
+ client.offset = offset
311
+ client.match_mode = match_mode
312
+ client.filters = filters
313
+ client.sort_mode = sort_mode
314
+ client.sort_by = sort_by
315
+ client.group_by = group_by if group_by
316
+ client.group_function = group_function if group_function
317
+ client.index_weights = index_weights
318
+ client.anchor = anchor
319
+
320
+ client
321
+ end
322
+
323
+ def retry_on_stale_index(&block)
324
+ stale_ids = []
325
+ retries = stale_retries
326
+
327
+ begin
328
+ options[:raise_on_stale] = retries > 0
329
+ block.call
330
+
331
+ # If ThinkingSphinx::Search#instances_from_matches found records in
332
+ # Sphinx but not in the DB and the :raise_on_stale option is set, this
333
+ # exception is raised. We retry a limited number of times, excluding the
334
+ # stale ids from the search.
335
+ rescue StaleIdsException => err
336
+ retries -= 1
337
+
338
+ # For logging
339
+ stale_ids |= err.ids
340
+ # ID exclusion
341
+ options[:without_ids] = Array(options[:without_ids]) | err.ids
342
+
343
+ log 'Sphinx Stale Ids (%s %s left): %s' % [
344
+ retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
345
+ ]
346
+ retry
347
+ end
348
+ end
349
+
350
+ def classes
351
+ @classes ||= options[:classes] || []
352
+ end
353
+
354
+ def one_class
355
+ @one_class ||= classes.length != 1 ? nil : classes.first
356
+ end
357
+
358
+ def query
359
+ @query ||= begin
360
+ q = @args.join(' ') << conditions_as_query
361
+ (options[:star] ? star_query(q) : q).strip
362
+ end
363
+ end
364
+
365
+ def conditions_as_query
366
+ return '' if @options[:conditions].blank?
367
+
368
+ # Soon to be deprecated.
369
+ keys = @options[:conditions].keys.reject { |key|
370
+ attributes.include?(key.to_sym)
371
+ }
372
+
373
+ ' ' + keys.collect { |key|
374
+ "@#{key} #{options[:conditions][key]}"
375
+ }.join(' ')
376
+ end
377
+
378
+ def star_query(query)
379
+ token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
380
+
381
+ query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
382
+ pre, proper, post = $`, $&, $'
383
+ # E.g. "@foo", "/2", "~3", but not as part of a token
384
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z})
385
+ # E.g. "foo bar", with quotes
386
+ is_quote = proper.starts_with?('"') && proper.ends_with?('"')
387
+ has_star = pre.ends_with?("*") || post.starts_with?("*")
388
+ if is_operator || is_quote || has_star
389
+ proper
390
+ else
391
+ "*#{proper}*"
392
+ end
393
+ end
394
+ end
395
+
396
+ def comment
397
+ options[:comment] || ''
398
+ end
399
+
400
+ def match_mode
401
+ options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
402
+ end
403
+
404
+ def sort_mode
405
+ @sort_mode ||= case options[:sort_mode]
406
+ when :asc
407
+ :attr_asc
408
+ when :desc
409
+ :attr_desc
410
+ when nil
411
+ case options[:order]
412
+ when String
413
+ :extended
414
+ when Symbol
415
+ :attr_asc
416
+ else
417
+ :relevance
418
+ end
419
+ else
420
+ options[:sort_mode]
421
+ end
422
+ end
423
+
424
+ def sort_by
425
+ case @sort_by = (options[:sort_by] || options[:order])
426
+ when String
427
+ sorted_fields_to_attributes(@sort_by)
428
+ when Symbol
429
+ field_names.include?(@sort_by) ?
430
+ @sort_by.to_s.concat('_sort') : @sort_by.to_s
431
+ else
432
+ ''
433
+ end
434
+ end
435
+
436
+ def field_names
437
+ return [] unless one_class
438
+
439
+ one_class.sphinx_indexes.collect { |index|
440
+ index.fields.collect { |field| field.unique_name }
441
+ }.flatten
442
+ end
443
+
444
+ def sorted_fields_to_attributes(order_string)
445
+ field_names.each { |field|
446
+ order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
447
+ match.gsub field.to_s, field.to_s.concat("_sort")
448
+ }
449
+ }
450
+
451
+ order_string
452
+ end
453
+
454
+ # Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
455
+ # { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
456
+ #
457
+ def index_weights
458
+ weights = options[:index_weights] || {}
459
+ weights.keys.inject({}) do |hash, key|
460
+ if key.is_a?(Class)
461
+ name = ThinkingSphinx::Index.name_for(key)
462
+ hash["#{name}_core"] = weights[key]
463
+ hash["#{name}_delta"] = weights[key]
464
+ else
465
+ hash[key] = weights[key]
466
+ end
467
+
468
+ hash
469
+ end
470
+ end
471
+
472
+ def group_by
473
+ options[:group] ? options[:group].to_s : nil
474
+ end
475
+
476
+ def group_function
477
+ options[:group] ? :attr : nil
478
+ end
479
+
480
+ def internal_filters
481
+ filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
482
+
483
+ class_crcs = classes.collect { |klass|
484
+ klass.to_crc32s
485
+ }.flatten
486
+
487
+ unless class_crcs.empty?
488
+ filters << Riddle::Client::Filter.new('class_crc', class_crcs)
489
+ end
490
+
491
+ filters << Riddle::Client::Filter.new(
492
+ 'sphinx_internal_id', filter_value(options[:without_ids]), true
493
+ ) if options[:without_ids]
494
+
495
+ filters
496
+ end
497
+
498
+ def condition_filters
499
+ (options[:conditions] || {}).collect { |attrib, value|
500
+ if attributes.include?(attrib.to_sym)
501
+ puts <<-MSG
502
+ Deprecation Warning: filters on attributes should be done using the :with
503
+ option, not :conditions. For example:
504
+ :with => {:#{attrib} => #{value.inspect}}
505
+ MSG
506
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
507
+ else
508
+ nil
509
+ end
510
+ }.compact
511
+ end
512
+
513
+ def filters
514
+ internal_filters +
515
+ condition_filters +
516
+ (options[:with] || {}).collect { |attrib, value|
517
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
518
+ } +
519
+ (options[:without] || {}).collect { |attrib, value|
520
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
521
+ } +
522
+ (options[:with_all] || {}).collect { |attrib, values|
523
+ Array(values).collect { |value|
524
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
525
+ }
526
+ }.flatten
527
+ end
528
+
529
+ # When passed a Time instance, returns the integer timestamp.
530
+ #
531
+ # If using Rails 2.1+, need to handle timezones to translate them back to
532
+ # UTC, as that's what datetimes will be stored as by MySQL.
533
+ #
534
+ # in_time_zone is a method that was added for the timezone support in
535
+ # Rails 2.1, which is why it's used for testing. I'm sure there's better
536
+ # ways, but this does the job.
537
+ #
538
+ def filter_value(value)
539
+ case value
540
+ when Range
541
+ filter_value(value.first).first..filter_value(value.last).first
542
+ when Array
543
+ value.collect { |v| filter_value(v) }.flatten
544
+ when Time
545
+ value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
546
+ when NilClass
547
+ 0
548
+ else
549
+ Array(value)
550
+ end
551
+ end
552
+
553
+ def anchor
554
+ return {} unless options[:geo] || (options[:lat] && options[:lng])
555
+
556
+ {
557
+ :latitude => options[:geo] ? options[:geo].first : options[:lat],
558
+ :longitude => options[:geo] ? options[:geo].last : options[:lng],
559
+ :latitude_attribute => latitude_attr.to_s,
560
+ :longitude_attribute => longitude_attr.to_s
561
+ }
562
+ end
563
+
564
+ def latitude_attr
565
+ options[:latitude_attr] ||
566
+ index_option(:latitude_attr) ||
567
+ attribute(:lat, :latitude)
568
+ end
569
+
570
+ def longitude_attr
571
+ options[:longitude_attr] ||
572
+ index_option(:longitude_attr) ||
573
+ attribute(:lon, :lng, :longitude)
574
+ end
575
+
576
+ def index_option(key)
577
+ return nil unless one_class
578
+
579
+ one_class.sphinx_indexes.collect { |index|
580
+ index.local_options[key]
581
+ }.compact.first
582
+ end
583
+
584
+ def attribute(*keys)
585
+ return nil unless one_class
586
+
587
+ keys.detect { |key|
588
+ attributes.include?(key)
589
+ }
590
+ end
591
+
592
+ def attributes
593
+ return [] unless one_class
594
+
595
+ attributes = one_class.sphinx_indexes.collect { |index|
596
+ index.attributes.collect { |attrib| attrib.unique_name }
597
+ }.flatten
598
+ end
599
+
600
+ def stale_retries
601
+ case options[:retry_stale]
602
+ when TrueClass
603
+ 3
604
+ when nil, FalseClass
605
+ 0
606
+ else
607
+ options[:retry_stale].to_i
608
+ end
609
+ end
610
+
611
+ def instances_from_class(klass, matches)
612
+ index_options = klass.sphinx_index_options
613
+
614
+ ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
615
+ instances = ids.length > 0 ? klass.find(
616
+ :all,
617
+ :joins => options[:joins],
618
+ :conditions => {klass.primary_key_for_sphinx.to_sym => ids},
619
+ :include => (options[:include] || index_options[:include]),
620
+ :select => (options[:select] || index_options[:select]),
621
+ :order => (options[:sql_order] || index_options[:sql_order])
622
+ ) : []
623
+
624
+ # Raise an exception if we find records in Sphinx but not in the DB, so
625
+ # the search method can retry without them. See
626
+ # ThinkingSphinx::Search.retry_search_on_stale_index.
627
+ if options[:raise_on_stale] && instances.length < ids.length
628
+ stale_ids = ids - instances.map { |i| i.id }
629
+ raise StaleIdsException, stale_ids
630
+ end
631
+
632
+ # if the user has specified an SQL order, return the collection
633
+ # without rearranging it into the Sphinx order
634
+ return instances if (options[:sql_order] || index_options[:sql_order])
635
+
636
+ ids.collect { |obj_id|
637
+ instances.detect do |obj|
638
+ obj.primary_key_for_sphinx == obj_id
639
+ end
640
+ }
641
+ end
642
+
643
+ # Group results by class and call #find(:all) once for each group to reduce
644
+ # the number of #find's in multi-model searches.
645
+ #
646
+ def instances_from_matches
647
+ return single_class_results if one_class
648
+
649
+ groups = results[:matches].group_by { |match|
650
+ match[:attributes]["class_crc"]
651
+ }
652
+ groups.each do |crc, group|
653
+ group.replace(
654
+ instances_from_class(class_from_crc(crc), group)
655
+ )
656
+ end
657
+
658
+ results[:matches].collect do |match|
659
+ groups.detect { |crc, group|
660
+ crc == match[:attributes]["class_crc"]
661
+ }[1].compact.detect { |obj|
662
+ obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
663
+ }
664
+ end
665
+ end
666
+
667
+ def single_class_results
668
+ instances_from_class one_class, results[:matches]
669
+ end
670
+
671
+ def class_from_crc(crc)
672
+ config.models_by_crc[crc].constantize
673
+ end
674
+
675
+ def each_with_attribute(attribute, &block)
676
+ populate
677
+ results[:matches].each_with_index do |match, index|
678
+ yield self[index],
679
+ (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
680
+ end
681
+ end
682
+
683
+ def is_scope?(method)
684
+ one_class && one_class.sphinx_scopes.include?(method)
685
+ end
686
+
687
+ def add_scope(method, *args, &block)
688
+ merge_search one_class.send(method, *args, &block)
689
+ end
690
+
691
+ def merge_search(search)
692
+ search.args.each { |arg| args << arg }
693
+
694
+ search.options.keys.each do |key|
695
+ if HashOptions.include?(key)
696
+ options[key] ||= {}
697
+ options[key].merge! search.options[key]
698
+ elsif ArrayOptions.include?(key)
699
+ options[key] ||= []
700
+ options[key] += search.options[key]
701
+ options[key].uniq!
702
+ else
703
+ options[key] = search.options[key]
704
+ end
705
+ end
706
+ end
707
+ end
708
+ end