skalee-thinking-sphinx 1.3.14.1

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 (199) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +201 -0
  3. data/Rakefile +3 -0
  4. data/VERSION +1 -0
  5. data/contribute.rb +385 -0
  6. data/cucumber.yml +1 -0
  7. data/features/abstract_inheritance.feature +10 -0
  8. data/features/alternate_primary_key.feature +27 -0
  9. data/features/attribute_transformation.feature +22 -0
  10. data/features/attribute_updates.feature +51 -0
  11. data/features/deleting_instances.feature +67 -0
  12. data/features/direct_attributes.feature +11 -0
  13. data/features/excerpts.feature +13 -0
  14. data/features/extensible_delta_indexing.feature +9 -0
  15. data/features/facets.feature +82 -0
  16. data/features/facets_across_model.feature +29 -0
  17. data/features/handling_edits.feature +92 -0
  18. data/features/retry_stale_indexes.feature +24 -0
  19. data/features/searching_across_models.feature +20 -0
  20. data/features/searching_by_index.feature +40 -0
  21. data/features/searching_by_model.feature +175 -0
  22. data/features/searching_with_find_arguments.feature +56 -0
  23. data/features/sphinx_detection.feature +25 -0
  24. data/features/sphinx_scopes.feature +42 -0
  25. data/features/step_definitions/alpha_steps.rb +16 -0
  26. data/features/step_definitions/beta_steps.rb +7 -0
  27. data/features/step_definitions/common_steps.rb +188 -0
  28. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  29. data/features/step_definitions/facet_steps.rb +96 -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 +15 -0
  33. data/features/step_definitions/search_steps.rb +89 -0
  34. data/features/step_definitions/sphinx_steps.rb +35 -0
  35. data/features/sti_searching.feature +19 -0
  36. data/features/support/database.example.yml +3 -0
  37. data/features/support/db/.gitignore +1 -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/developers.rb +29 -0
  46. data/features/support/db/fixtures/dogs.rb +3 -0
  47. data/features/support/db/fixtures/extensible_betas.rb +10 -0
  48. data/features/support/db/fixtures/foxes.rb +3 -0
  49. data/features/support/db/fixtures/gammas.rb +10 -0
  50. data/features/support/db/fixtures/music.rb +4 -0
  51. data/features/support/db/fixtures/people.rb +1001 -0
  52. data/features/support/db/fixtures/posts.rb +6 -0
  53. data/features/support/db/fixtures/robots.rb +14 -0
  54. data/features/support/db/fixtures/tags.rb +27 -0
  55. data/features/support/db/migrations/create_alphas.rb +8 -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_developers.rb +9 -0
  64. data/features/support/db/migrations/create_extensible_betas.rb +5 -0
  65. data/features/support/db/migrations/create_gammas.rb +3 -0
  66. data/features/support/db/migrations/create_genres.rb +3 -0
  67. data/features/support/db/migrations/create_music.rb +6 -0
  68. data/features/support/db/migrations/create_people.rb +13 -0
  69. data/features/support/db/migrations/create_posts.rb +5 -0
  70. data/features/support/db/migrations/create_robots.rb +4 -0
  71. data/features/support/db/migrations/create_taggings.rb +5 -0
  72. data/features/support/db/migrations/create_tags.rb +4 -0
  73. data/features/support/env.rb +21 -0
  74. data/features/support/lib/generic_delta_handler.rb +8 -0
  75. data/features/support/models/alpha.rb +22 -0
  76. data/features/support/models/animal.rb +5 -0
  77. data/features/support/models/author.rb +3 -0
  78. data/features/support/models/beta.rb +8 -0
  79. data/features/support/models/box.rb +8 -0
  80. data/features/support/models/cat.rb +3 -0
  81. data/features/support/models/category.rb +4 -0
  82. data/features/support/models/comment.rb +10 -0
  83. data/features/support/models/developer.rb +16 -0
  84. data/features/support/models/dog.rb +3 -0
  85. data/features/support/models/extensible_beta.rb +9 -0
  86. data/features/support/models/fox.rb +5 -0
  87. data/features/support/models/gamma.rb +5 -0
  88. data/features/support/models/genre.rb +3 -0
  89. data/features/support/models/medium.rb +5 -0
  90. data/features/support/models/music.rb +8 -0
  91. data/features/support/models/person.rb +23 -0
  92. data/features/support/models/post.rb +21 -0
  93. data/features/support/models/robot.rb +12 -0
  94. data/features/support/models/tag.rb +3 -0
  95. data/features/support/models/tagging.rb +4 -0
  96. data/ginger_scenarios.rb +28 -0
  97. data/init.rb +5 -0
  98. data/install.rb +5 -0
  99. data/lib/cucumber/thinking_sphinx/external_world.rb +8 -0
  100. data/lib/cucumber/thinking_sphinx/internal_world.rb +126 -0
  101. data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
  102. data/lib/thinking_sphinx/active_record/attribute_updates.rb +19 -0
  103. data/lib/thinking_sphinx/active_record/delta.rb +47 -0
  104. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  105. data/lib/thinking_sphinx/active_record/scopes.rb +75 -0
  106. data/lib/thinking_sphinx/active_record.rb +348 -0
  107. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  108. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  109. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +143 -0
  110. data/lib/thinking_sphinx/association.rb +164 -0
  111. data/lib/thinking_sphinx/attribute.rb +362 -0
  112. data/lib/thinking_sphinx/auto_version.rb +22 -0
  113. data/lib/thinking_sphinx/class_facet.rb +15 -0
  114. data/lib/thinking_sphinx/configuration.rb +300 -0
  115. data/lib/thinking_sphinx/context.rb +68 -0
  116. data/lib/thinking_sphinx/core/array.rb +7 -0
  117. data/lib/thinking_sphinx/core/string.rb +15 -0
  118. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  119. data/lib/thinking_sphinx/deltas.rb +28 -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 +136 -0
  124. data/lib/thinking_sphinx/field.rb +82 -0
  125. data/lib/thinking_sphinx/index/builder.rb +296 -0
  126. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  127. data/lib/thinking_sphinx/index.rb +157 -0
  128. data/lib/thinking_sphinx/property.rb +162 -0
  129. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  130. data/lib/thinking_sphinx/search.rb +769 -0
  131. data/lib/thinking_sphinx/search_methods.rb +439 -0
  132. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  133. data/lib/thinking_sphinx/source/sql.rb +130 -0
  134. data/lib/thinking_sphinx/source.rb +153 -0
  135. data/lib/thinking_sphinx/tasks.rb +131 -0
  136. data/lib/thinking_sphinx/test.rb +52 -0
  137. data/lib/thinking_sphinx.rb +225 -0
  138. data/rails/init.rb +16 -0
  139. data/recipes/thinking_sphinx.rb +3 -0
  140. data/spec/fixtures/data.sql +32 -0
  141. data/spec/fixtures/database.yml.default +3 -0
  142. data/spec/fixtures/models.rb +145 -0
  143. data/spec/fixtures/structure.sql +125 -0
  144. data/spec/spec_helper.rb +60 -0
  145. data/spec/sphinx_helper.rb +81 -0
  146. data/spec/thinking_sphinx/active_record/delta_spec.rb +128 -0
  147. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +55 -0
  148. data/spec/thinking_sphinx/active_record/scopes_spec.rb +177 -0
  149. data/spec/thinking_sphinx/active_record_spec.rb +622 -0
  150. data/spec/thinking_sphinx/association_spec.rb +239 -0
  151. data/spec/thinking_sphinx/attribute_spec.rb +570 -0
  152. data/spec/thinking_sphinx/auto_version_spec.rb +39 -0
  153. data/spec/thinking_sphinx/configuration_spec.rb +234 -0
  154. data/spec/thinking_sphinx/context_spec.rb +119 -0
  155. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  156. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  157. data/spec/thinking_sphinx/excerpter_spec.rb +57 -0
  158. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  159. data/spec/thinking_sphinx/facet_spec.rb +333 -0
  160. data/spec/thinking_sphinx/field_spec.rb +154 -0
  161. data/spec/thinking_sphinx/index/builder_spec.rb +479 -0
  162. data/spec/thinking_sphinx/index/faux_column_spec.rb +30 -0
  163. data/spec/thinking_sphinx/index_spec.rb +183 -0
  164. data/spec/thinking_sphinx/rails_additions_spec.rb +203 -0
  165. data/spec/thinking_sphinx/search_methods_spec.rb +152 -0
  166. data/spec/thinking_sphinx/search_spec.rb +1181 -0
  167. data/spec/thinking_sphinx/source_spec.rb +235 -0
  168. data/spec/thinking_sphinx_spec.rb +204 -0
  169. data/tasks/distribution.rb +41 -0
  170. data/tasks/rails.rake +1 -0
  171. data/tasks/testing.rb +72 -0
  172. data/vendor/after_commit/.gitignore +1 -0
  173. data/vendor/after_commit/lib/after_commit/active_record.rb +122 -0
  174. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +168 -0
  175. data/vendor/after_commit/lib/after_commit/test_bypass.rb +30 -0
  176. data/vendor/after_commit/lib/after_commit.rb +70 -0
  177. data/vendor/riddle/lib/riddle/0.9.8.rb +1 -0
  178. data/vendor/riddle/lib/riddle/0.9.9/client/filter.rb +22 -0
  179. data/vendor/riddle/lib/riddle/0.9.9/client.rb +49 -0
  180. data/vendor/riddle/lib/riddle/0.9.9/configuration/searchd.rb +28 -0
  181. data/vendor/riddle/lib/riddle/0.9.9.rb +7 -0
  182. data/vendor/riddle/lib/riddle/auto_version.rb +11 -0
  183. data/vendor/riddle/lib/riddle/client/filter.rb +62 -0
  184. data/vendor/riddle/lib/riddle/client/message.rb +70 -0
  185. data/vendor/riddle/lib/riddle/client/response.rb +94 -0
  186. data/vendor/riddle/lib/riddle/client.rb +745 -0
  187. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +49 -0
  188. data/vendor/riddle/lib/riddle/configuration/index.rb +149 -0
  189. data/vendor/riddle/lib/riddle/configuration/indexer.rb +20 -0
  190. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  191. data/vendor/riddle/lib/riddle/configuration/searchd.rb +28 -0
  192. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  193. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  194. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +53 -0
  195. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +29 -0
  196. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  197. data/vendor/riddle/lib/riddle/controller.rb +78 -0
  198. data/vendor/riddle/lib/riddle.rb +51 -0
  199. metadata +312 -0
@@ -0,0 +1,19 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module AttributeUpdates
4
+
5
+ private
6
+
7
+ def updatable_attributes(index)
8
+ index.attributes.select { |attrib| attrib.updatable? }
9
+ end
10
+
11
+ def attribute_values_for_index(index)
12
+ updatable_attributes(index).inject({}) { |hash, attrib|
13
+ hash[attrib.unique_name.to_s] = attrib.live_value self
14
+ hash
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ # This module contains all the delta-related code for models. There isn't
4
+ # really anything you need to call manually in here - except perhaps
5
+ # index_delta, but not sure what reason why.
6
+ #
7
+ module Delta
8
+ # Code for after_commit callback is written by Eli Miller:
9
+ # http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html
10
+ # with slight modification from Joost Hietbrink.
11
+ #
12
+ def self.included(base)
13
+ base.class_eval do
14
+ class << self
15
+ def delta_object
16
+ self.sphinx_indexes.first.delta_object
17
+ end
18
+ end
19
+
20
+ def toggled_delta?
21
+ self.class.delta_object.toggled(self)
22
+ end
23
+
24
+ private
25
+
26
+ # Set the delta value for the model to be true.
27
+ def toggle_delta
28
+ self.class.delta_object.toggle(self) if should_toggle_delta?
29
+ end
30
+
31
+ def should_toggle_delta?
32
+ self.new_record? || indexed_data_changed?
33
+ end
34
+
35
+ def indexed_data_changed?
36
+ sphinx_indexes.any? { |index|
37
+ index.fields.any? { |field| field.changed?(self) } ||
38
+ index.attributes.any? { |attrib|
39
+ attrib.public? && attrib.changed?(self) && !attrib.updatable?
40
+ }
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module HasManyAssociation
4
+ def search(*args)
5
+ foreign_key = @reflection.primary_key_name
6
+ stack = [@reflection.options[:through]].compact
7
+
8
+ attribute = nil
9
+ @reflection.klass.define_indexes
10
+ (@reflection.klass.sphinx_indexes || []).each do |index|
11
+ attribute = index.attributes.detect { |attrib|
12
+ attrib.columns.length == 1 &&
13
+ attrib.columns.first.__name == foreign_key.to_sym
14
+ }
15
+ break if attribute
16
+ end
17
+
18
+ raise "Missing Attribute for Foreign Key #{foreign_key}" unless attribute
19
+
20
+ options = args.extract_options!
21
+ options[:with] ||= {}
22
+ options[:with][attribute.unique_name] = @owner.id
23
+
24
+ args << options
25
+ @reflection.klass.search(*args)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,75 @@
1
+ module ThinkingSphinx
2
+ module ActiveRecord
3
+ module Scopes
4
+ def self.included(base)
5
+ base.class_eval do
6
+ extend ThinkingSphinx::ActiveRecord::Scopes::ClassMethods
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Similar to ActiveRecord's default_scope method Thinking Sphinx supports
13
+ # a default_sphinx_scope. For example:
14
+ #
15
+ # default_sphinx_scope :some_sphinx_named_scope
16
+ #
17
+ # The scope is automatically applied when the search method is called. It
18
+ # will only be applied if it is an existing sphinx_scope.
19
+ def default_sphinx_scope(sphinx_scope_name)
20
+ @default_sphinx_scope = sphinx_scope_name
21
+ end
22
+
23
+ # Returns the default_sphinx_scope or nil if none is set.
24
+ def get_default_sphinx_scope
25
+ @default_sphinx_scope
26
+ end
27
+
28
+ # Returns true if the current Model has a default_sphinx_scope. Also checks if
29
+ # the default_sphinx_scope actually is a scope.
30
+ def has_default_sphinx_scope?
31
+ !@default_sphinx_scope.nil? && sphinx_scopes.include?(@default_sphinx_scope)
32
+ end
33
+
34
+ # Similar to ActiveRecord's named_scope method Thinking Sphinx supports
35
+ # scopes. For example:
36
+ #
37
+ # sphinx_scope(:latest_first) {
38
+ # {:order => 'created_at DESC, @relevance DESC'}
39
+ # }
40
+ #
41
+ # Usage:
42
+ #
43
+ # @articles = Article.latest_first.search 'pancakes'
44
+ #
45
+ def sphinx_scope(method, &block)
46
+ @sphinx_scopes ||= []
47
+ @sphinx_scopes << method
48
+
49
+ singleton_class.instance_eval do
50
+ define_method(method) do |*args|
51
+ options = {:classes => classes_option}
52
+ options.merge! block.call(*args)
53
+
54
+ ThinkingSphinx::Search.new(options)
55
+ end
56
+ end
57
+ end
58
+
59
+ # This returns an Array of all defined scopes. The default
60
+ # scope shows as :default.
61
+ def sphinx_scopes
62
+ @sphinx_scopes || []
63
+ end
64
+
65
+ def remove_sphinx_scopes
66
+ sphinx_scopes.each do |scope|
67
+ singleton_class.send(:undef_method, scope)
68
+ end
69
+
70
+ sphinx_scopes.clear
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,348 @@
1
+ require 'thinking_sphinx/active_record/attribute_updates'
2
+ require 'thinking_sphinx/active_record/delta'
3
+ require 'thinking_sphinx/active_record/has_many_association'
4
+ require 'thinking_sphinx/active_record/scopes'
5
+
6
+ module ThinkingSphinx
7
+ # Core additions to ActiveRecord models - define_index for creating indexes
8
+ # for models. If you want to interrogate the index objects created for the
9
+ # model, you can use the class-level accessor :sphinx_indexes.
10
+ #
11
+ module ActiveRecord
12
+ def self.included(base)
13
+ base.class_eval do
14
+ class_inheritable_array :sphinx_indexes, :sphinx_facets
15
+
16
+ extend ThinkingSphinx::ActiveRecord::ClassMethods
17
+
18
+ class << self
19
+ attr_accessor :sphinx_index_blocks
20
+
21
+ def set_sphinx_primary_key(attribute)
22
+ @sphinx_primary_key_attribute = attribute
23
+ end
24
+
25
+ def primary_key_for_sphinx
26
+ @sphinx_primary_key_attribute || primary_key
27
+ end
28
+
29
+ def sphinx_index_options
30
+ sphinx_indexes.last.options
31
+ end
32
+
33
+ # Generate a unique CRC value for the model's name, to use to
34
+ # determine which Sphinx documents belong to which AR records.
35
+ #
36
+ # Really only written for internal use - but hey, if it's useful to
37
+ # you in some other way, awesome.
38
+ #
39
+ def to_crc32
40
+ self.name.to_crc32
41
+ end
42
+
43
+ def to_crc32s
44
+ (subclasses << self).collect { |klass| klass.to_crc32 }
45
+ end
46
+
47
+ def sphinx_database_adapter
48
+ @sphinx_database_adapter ||=
49
+ ThinkingSphinx::AbstractAdapter.detect(self)
50
+ end
51
+
52
+ def sphinx_name
53
+ self.name.underscore.tr(':/\\', '_')
54
+ end
55
+
56
+ #
57
+ # The above method to_crc32s is dependant on the subclasses being loaded consistently
58
+ # After a reset_subclasses is called (during a Dispatcher.cleanup_application in development)
59
+ # Our subclasses will be lost but our context will not reload them for us.
60
+ #
61
+ # We reset the context which causes the subclasses to be reloaded next time the context is called.
62
+ #
63
+ def reset_subclasses_with_thinking_sphinx
64
+ reset_subclasses_without_thinking_sphinx
65
+ ThinkingSphinx.reset_context!
66
+ end
67
+
68
+ alias_method_chain :reset_subclasses, :thinking_sphinx
69
+
70
+ private
71
+
72
+ def defined_indexes?
73
+ @defined_indexes
74
+ end
75
+
76
+ def defined_indexes=(value)
77
+ @defined_indexes = value
78
+ end
79
+
80
+ def sphinx_delta?
81
+ self.sphinx_indexes.any? { |index| index.delta? }
82
+ end
83
+ end
84
+ end
85
+
86
+ ::ActiveRecord::Associations::HasManyAssociation.send(
87
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
88
+ )
89
+ ::ActiveRecord::Associations::HasManyThroughAssociation.send(
90
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
91
+ )
92
+ end
93
+
94
+ module ClassMethods
95
+ # Allows creation of indexes for Sphinx. If you don't do this, there
96
+ # isn't much point trying to search (or using this plugin at all,
97
+ # really).
98
+ #
99
+ # An example or two:
100
+ #
101
+ # define_index
102
+ # indexes :id, :as => :model_id
103
+ # indexes name
104
+ # end
105
+ #
106
+ # You can also grab fields from associations - multiple levels deep
107
+ # if necessary.
108
+ #
109
+ # define_index do
110
+ # indexes tags.name, :as => :tag
111
+ # indexes articles.content
112
+ # indexes orders.line_items.product.name, :as => :product
113
+ # end
114
+ #
115
+ # And it will automatically concatenate multiple fields:
116
+ #
117
+ # define_index do
118
+ # indexes [author.first_name, author.last_name], :as => :author
119
+ # end
120
+ #
121
+ # The #indexes method is for fields - if you want attributes, use
122
+ # #has instead. All the same rules apply - but keep in mind that
123
+ # attributes are for sorting, grouping and filtering, not searching.
124
+ #
125
+ # define_index do
126
+ # # fields ...
127
+ #
128
+ # has created_at, updated_at
129
+ # end
130
+ #
131
+ # One last feature is the delta index. This requires the model to
132
+ # have a boolean field named 'delta', and is enabled as follows:
133
+ #
134
+ # define_index do
135
+ # # fields ...
136
+ # # attributes ...
137
+ #
138
+ # set_property :delta => true
139
+ # end
140
+ #
141
+ # Check out the more detailed documentation for each of these methods
142
+ # at ThinkingSphinx::Index::Builder.
143
+ #
144
+ def define_index(name = nil, &block)
145
+ self.sphinx_index_blocks ||= []
146
+ self.sphinx_indexes ||= []
147
+ self.sphinx_facets ||= []
148
+
149
+ ThinkingSphinx.context.add_indexed_model self
150
+
151
+ if sphinx_index_blocks.empty?
152
+ before_validation :define_indexes
153
+ before_destroy :define_indexes
154
+ end
155
+
156
+ self.sphinx_index_blocks << lambda {
157
+ index = ThinkingSphinx::Index::Builder.generate self, name, &block
158
+ add_sphinx_callbacks_and_extend(index.delta?)
159
+ add_sphinx_index index
160
+ }
161
+
162
+ end
163
+
164
+ def define_indexes
165
+ superclass.define_indexes unless superclass == ::ActiveRecord::Base
166
+
167
+ return if sphinx_index_blocks.nil? ||
168
+ defined_indexes? ||
169
+ !ThinkingSphinx.define_indexes?
170
+
171
+ sphinx_index_blocks.each do |block|
172
+ block.call
173
+ end
174
+
175
+ self.defined_indexes = true
176
+
177
+ # We want to make sure that if the database doesn't exist, then Thinking
178
+ # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
179
+ # and db:migrate). It's a bit hacky, but I can't think of a better way.
180
+ rescue StandardError => err
181
+ case err.class.name
182
+ when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
183
+ return
184
+ else
185
+ raise err
186
+ end
187
+ end
188
+
189
+ def add_sphinx_index(index)
190
+ self.sphinx_indexes << index
191
+ subclasses.each { |klass| klass.add_sphinx_index(index) }
192
+ end
193
+
194
+ def has_sphinx_indexes?
195
+ sphinx_indexes &&
196
+ sphinx_index_blocks &&
197
+ (sphinx_indexes.length > 0 || sphinx_index_blocks.length > 0)
198
+ end
199
+
200
+ def indexed_by_sphinx?
201
+ sphinx_indexes && sphinx_indexes.length > 0
202
+ end
203
+
204
+ def delta_indexed_by_sphinx?
205
+ sphinx_indexes && sphinx_indexes.any? { |index| index.delta? }
206
+ end
207
+
208
+ def sphinx_index_names
209
+ define_indexes
210
+ sphinx_indexes.collect(&:all_names).flatten
211
+ end
212
+
213
+ def core_index_names
214
+ define_indexes
215
+ sphinx_indexes.collect(&:core_name)
216
+ end
217
+
218
+ def delta_index_names
219
+ define_indexes
220
+ sphinx_indexes.select(&:delta?).collect(&:delta_name)
221
+ end
222
+
223
+ def to_riddle
224
+ define_indexes
225
+ sphinx_database_adapter.setup
226
+
227
+ local_sphinx_indexes.collect { |index|
228
+ index.to_riddle(sphinx_offset)
229
+ }.flatten
230
+ end
231
+
232
+ def source_of_sphinx_index
233
+ define_indexes
234
+ possible_models = self.sphinx_indexes.collect { |index| index.model }
235
+ return self if possible_models.include?(self)
236
+
237
+ parent = self.superclass
238
+ while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
239
+ parent = parent.superclass
240
+ end
241
+
242
+ return parent
243
+ end
244
+
245
+ def delete_in_index(index, document_id)
246
+ return unless ThinkingSphinx.sphinx_running? &&
247
+ search_for_id(document_id, index)
248
+
249
+ ThinkingSphinx::Configuration.instance.client.update(
250
+ index, ['sphinx_deleted'], {document_id => [1]}
251
+ )
252
+ end
253
+
254
+ def sphinx_offset
255
+ ThinkingSphinx.context.superclass_indexed_models.
256
+ index eldest_indexed_ancestor
257
+ end
258
+
259
+ # Temporarily disable delta indexing inside a block, then perform a single
260
+ # rebuild of index at the end.
261
+ #
262
+ # Useful when performing updates to batches of models to prevent
263
+ # the delta index being rebuilt after each individual update.
264
+ #
265
+ # In the following example, the delta index will only be rebuilt once,
266
+ # not 10 times.
267
+ #
268
+ # SomeModel.suspended_delta do
269
+ # 10.times do
270
+ # SomeModel.create( ... )
271
+ # end
272
+ # end
273
+ #
274
+ def suspended_delta(reindex_after = true, &block)
275
+ define_indexes
276
+ original_setting = ThinkingSphinx.deltas_enabled?
277
+ ThinkingSphinx.deltas_enabled = false
278
+ begin
279
+ yield
280
+ ensure
281
+ ThinkingSphinx.deltas_enabled = original_setting
282
+ end
283
+ end
284
+
285
+ private
286
+
287
+ def local_sphinx_indexes
288
+ sphinx_indexes.select { |index|
289
+ index.model == self
290
+ }
291
+ end
292
+
293
+ def add_sphinx_callbacks_and_extend(delta = false)
294
+
295
+ if delta && !delta_indexed_by_sphinx?
296
+ before_save :toggle_delta
297
+ end
298
+ end
299
+
300
+ def eldest_indexed_ancestor
301
+ ancestors.reverse.detect { |ancestor|
302
+ ThinkingSphinx.context.indexed_models.include?(ancestor.name)
303
+ }.name
304
+ end
305
+ end
306
+
307
+ def in_index?(suffix)
308
+ self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
309
+ end
310
+
311
+ def in_core_index?
312
+ in_index? "core"
313
+ end
314
+
315
+ def in_delta_index?
316
+ in_index? "delta"
317
+ end
318
+
319
+ def in_both_indexes?
320
+ in_core_index? && in_delta_index?
321
+ end
322
+
323
+ # Returns the unique integer id for the object. This method uses the
324
+ # attribute hash to get around ActiveRecord always mapping the #id method
325
+ # to whatever the real primary key is (which may be a unique string hash).
326
+ #
327
+ # @return [Integer] Unique record id for the purposes of Sphinx.
328
+ #
329
+ def primary_key_for_sphinx
330
+ @primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx)
331
+ end
332
+
333
+ def sphinx_document_id
334
+ primary_key_for_sphinx * ThinkingSphinx.context.indexed_models.size +
335
+ self.class.sphinx_offset
336
+ end
337
+
338
+ private
339
+
340
+ def sphinx_index_name(suffix)
341
+ "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
342
+ end
343
+
344
+ def define_indexes
345
+ self.class.define_indexes
346
+ end
347
+ end
348
+ end
@@ -0,0 +1,42 @@
1
+ module ThinkingSphinx
2
+ class AbstractAdapter
3
+ def initialize(model)
4
+ @model = model
5
+ end
6
+
7
+ def setup
8
+ # Deliberately blank - subclasses should do something though. Well, if
9
+ # they need to.
10
+ end
11
+
12
+ def self.detect(model)
13
+ case model.connection.class.name
14
+ when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
15
+ "ActiveRecord::ConnectionAdapters::MysqlplusAdapter"
16
+ ThinkingSphinx::MysqlAdapter.new model
17
+ when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
18
+ ThinkingSphinx::PostgreSQLAdapter.new model
19
+ when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
20
+ if model.connection.config[:adapter] == "jdbcmysql"
21
+ ThinkingSphinx::MysqlAdapter.new model
22
+ elsif model.connection.config[:adapter] == "jdbcpostgresql"
23
+ ThinkingSphinx::PostgreSQLAdapter.new model
24
+ else
25
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
26
+ end
27
+ else
28
+ raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
29
+ end
30
+ end
31
+
32
+ def quote_with_table(column)
33
+ "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
34
+ end
35
+
36
+ protected
37
+
38
+ def connection
39
+ @connection ||= @model.connection
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ module ThinkingSphinx
2
+ class MysqlAdapter < AbstractAdapter
3
+ def setup
4
+ # Does MySQL actually need to do anything?
5
+ end
6
+
7
+ def sphinx_identifier
8
+ "mysql"
9
+ end
10
+
11
+ def concatenate(clause, separator = ' ')
12
+ "CONCAT_WS('#{separator}', #{clause})"
13
+ end
14
+
15
+ def group_concatenate(clause, separator = ' ')
16
+ "GROUP_CONCAT(DISTINCT IFNULL(#{clause}, '0') SEPARATOR '#{separator}')"
17
+ end
18
+
19
+ def cast_to_string(clause)
20
+ "CAST(#{clause} AS CHAR)"
21
+ end
22
+
23
+ def cast_to_datetime(clause)
24
+ "UNIX_TIMESTAMP(#{clause})"
25
+ end
26
+
27
+ def cast_to_unsigned(clause)
28
+ "CAST(#{clause} AS UNSIGNED)"
29
+ end
30
+
31
+ def convert_nulls(clause, default = '')
32
+ default = "'#{default}'" if default.is_a?(String)
33
+
34
+ "IFNULL(#{clause}, #{default})"
35
+ end
36
+
37
+ def boolean(value)
38
+ value ? 1 : 0
39
+ end
40
+
41
+ def crc(clause, blank_to_null = false)
42
+ clause = "NULLIF(#{clause},'')" if blank_to_null
43
+ "CRC32(#{clause})"
44
+ end
45
+
46
+ def utf8_query_pre
47
+ "SET NAMES utf8"
48
+ end
49
+
50
+ def time_difference(diff)
51
+ "DATE_SUB(NOW(), INTERVAL #{diff} SECOND)"
52
+ end
53
+ end
54
+ end