skalee-thinking-sphinx 1.3.14.1

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