factorylabs-thinking-sphinx 1.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +156 -0
  3. data/VERSION.yml +4 -0
  4. data/lib/thinking_sphinx.rb +210 -0
  5. data/lib/thinking_sphinx/active_record.rb +298 -0
  6. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  7. data/lib/thinking_sphinx/active_record/delta.rb +87 -0
  8. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  9. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  10. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  11. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  12. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +136 -0
  13. data/lib/thinking_sphinx/association.rb +164 -0
  14. data/lib/thinking_sphinx/attribute.rb +329 -0
  15. data/lib/thinking_sphinx/class_facet.rb +15 -0
  16. data/lib/thinking_sphinx/configuration.rb +282 -0
  17. data/lib/thinking_sphinx/core/string.rb +15 -0
  18. data/lib/thinking_sphinx/deltas.rb +30 -0
  19. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  20. data/lib/thinking_sphinx/deltas/default_delta.rb +68 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta.rb +30 -0
  22. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  23. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  24. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  25. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  26. data/lib/thinking_sphinx/excerpter.rb +22 -0
  27. data/lib/thinking_sphinx/facet.rb +108 -0
  28. data/lib/thinking_sphinx/facet_search.rb +134 -0
  29. data/lib/thinking_sphinx/field.rb +82 -0
  30. data/lib/thinking_sphinx/index.rb +99 -0
  31. data/lib/thinking_sphinx/index/builder.rb +287 -0
  32. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  33. data/lib/thinking_sphinx/property.rb +160 -0
  34. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  35. data/lib/thinking_sphinx/search.rb +671 -0
  36. data/lib/thinking_sphinx/search_methods.rb +421 -0
  37. data/lib/thinking_sphinx/source.rb +150 -0
  38. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  39. data/lib/thinking_sphinx/source/sql.rb +128 -0
  40. data/lib/thinking_sphinx/tasks.rb +165 -0
  41. data/rails/init.rb +14 -0
  42. data/spec/lib/thinking_sphinx/active_record/delta_spec.rb +136 -0
  43. data/spec/lib/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  44. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  45. data/spec/lib/thinking_sphinx/active_record_spec.rb +354 -0
  46. data/spec/lib/thinking_sphinx/association_spec.rb +246 -0
  47. data/spec/lib/thinking_sphinx/attribute_spec.rb +465 -0
  48. data/spec/lib/thinking_sphinx/configuration_spec.rb +268 -0
  49. data/spec/lib/thinking_sphinx/core/string_spec.rb +9 -0
  50. data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
  51. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  52. data/spec/lib/thinking_sphinx/facet_spec.rb +302 -0
  53. data/spec/lib/thinking_sphinx/field_spec.rb +154 -0
  54. data/spec/lib/thinking_sphinx/index/builder_spec.rb +355 -0
  55. data/spec/lib/thinking_sphinx/index/faux_column_spec.rb +30 -0
  56. data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
  57. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +203 -0
  58. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  59. data/spec/lib/thinking_sphinx/search_spec.rb +993 -0
  60. data/spec/lib/thinking_sphinx/source_spec.rb +217 -0
  61. data/spec/lib/thinking_sphinx_spec.rb +161 -0
  62. data/tasks/distribution.rb +49 -0
  63. data/tasks/rails.rake +1 -0
  64. data/tasks/testing.rb +78 -0
  65. data/vendor/after_commit/LICENSE +20 -0
  66. data/vendor/after_commit/README +16 -0
  67. data/vendor/after_commit/Rakefile +22 -0
  68. data/vendor/after_commit/init.rb +8 -0
  69. data/vendor/after_commit/lib/after_commit.rb +45 -0
  70. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  71. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  72. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  73. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  74. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  75. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  76. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  77. data/vendor/riddle/lib/riddle.rb +30 -0
  78. data/vendor/riddle/lib/riddle/client.rb +622 -0
  79. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  80. data/vendor/riddle/lib/riddle/client/message.rb +66 -0
  81. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  82. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  83. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  84. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  85. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  86. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  87. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  88. data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
  89. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  90. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  91. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  92. data/vendor/riddle/lib/riddle/controller.rb +54 -0
  93. metadata +169 -0
data/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Pat Allan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,156 @@
1
+ h1. Thinking Sphinx
2
+
3
+ h2. Usage
4
+
5
+ First, if you haven't done so already, check out the main "usage":http://ts.freelancing-gods.com/usage.html page. Once you've done that, the next place to look for information is the specific method docs - ThinkingSphinx::Search and ThinkingSphinx::Index::Builder in particular.
6
+
7
+ Keep in mind that while Thinking Sphinx works for ActiveRecord with Merb, it doesn't yet support DataMapper (although that is planned).
8
+
9
+ h2. Contributing
10
+
11
+ Fork on GitHub and after you've committed tested patches, send a pull request.
12
+
13
+ To quickly see if your system is ready to run the thinking sphinx specs, run the contribute.rb script found in the project root directory. Use the following instructions to install any missing requirements.
14
+
15
+ To get the spec suite running, you will need to install the not-a-mock gem if you don't already have it:
16
+
17
+ bc.git clone git://github.com/freelancing-god/not-a-mock.git
18
+ cd not-a-mock
19
+ rake gem
20
+ gem install pkg/not_a_mock-1.1.0.gem
21
+
22
+ Then install the ginger gem. The steps are the same, except that you might need to sudo the gem install:
23
+
24
+ bc.git clone git://github.com/freelancing-god/ginger.git
25
+ cd ginger
26
+ rake gem
27
+ sudo gem install pkg/ginger-1.1.0.gem
28
+
29
+ Alternatively, install the ginger gem directly from the freelancing-god github repository
30
+
31
+ bc.sudo gem sources -a http://gems.github.com
32
+ sudo gem install freelancing-god-ginger
33
+
34
+ Then install the cucumber, yard, jeweler and rspec gems. Make sure you have a git install version 1.6.0.0 or higher, otherwise the jeweler gem won't install.
35
+
36
+ bc.sudo gem install cucumber yard jeweler rspec
37
+
38
+ Then set up your database:
39
+
40
+ bc.cp spec/fixtures/database.yml.default spec/fixtures/database.yml
41
+ mysqladmin -u root create thinking_sphinx
42
+
43
+ This last step can be done automatically by the contribute.rb script if all dependencies are met.
44
+
45
+ Make sure you don't have another Sphinx daemon (searchd) running. If you do, quit it with "rake ts:stop"
46
+ in the app root.
47
+
48
+ You should now have a passing test suite from which to build your patch on.
49
+
50
+ bc.rake spec
51
+
52
+ If you get the message "Failed to start searchd daemon", run the spec with sudo:
53
+
54
+ bc.sudo rake spec
55
+
56
+ If you quit the spec suite before it's completed, you may be left with data in the test
57
+ database, causing the next run to have failures. Let that run complete and then try again.
58
+
59
+ h2. Contributors
60
+
61
+ Since I first released this library, there's been quite a few people who have submitted patches, to my immense gratitude. Others have suggested syntax changes and general improvements. So my thanks to the following people:
62
+
63
+ * Joost Hietbrink
64
+ * Jonathan Conway
65
+ * Gregory Mirzayantz
66
+ * Tung Nguyen
67
+ * Sean Cribbs
68
+ * Benoit Caccinolo
69
+ * John Barton
70
+ * Oliver Beddows
71
+ * Arthur Zapparoli
72
+ * Dusty Doris
73
+ * Marcus Crafter
74
+ * Patrick Lenz
75
+ * Björn Andreasson
76
+ * James Healy
77
+ * Jae-Jun Hwang
78
+ * Xavier Shay
79
+ * Jason Rust
80
+ * Gopal Patel
81
+ * Chris Heald
82
+ * Peter Vandenberk
83
+ * Josh French
84
+ * Andrew Bennett
85
+ * Jordan Fowler
86
+ * Seth Walker
87
+ * Joe Noon
88
+ * Wolfgang Postler
89
+ * Rick Olson
90
+ * Killian Murphy
91
+ * Morten Primdahl
92
+ * Ryan Bates
93
+ * David Eisinger
94
+ * Shay Arnett
95
+ * Minh Tran
96
+ * Jeremy Durham
97
+ * Piotr Sarnacki
98
+ * Matt Johnson
99
+ * Nicolas Blanco
100
+ * Max Lapshin
101
+ * Josh Natanson
102
+ * Philip Hallstrom
103
+ * Christian Rishøj
104
+ * Mike Flester
105
+ * Jim Remsik
106
+ * Kennon Ballou
107
+ * Henrik Nyh
108
+ * Emil Tin
109
+ * Doug Cole
110
+ * Ed Hickey
111
+ * Evan Weaver
112
+ * Thibaut Barrere
113
+ * Kristopher Chambers
114
+ * Dmitrij Smalko
115
+ * Aleksey Yeschenko
116
+ * Lachie Cox
117
+ * Lourens Naude
118
+ * Tom Davies
119
+ * Dan Pickett
120
+ * Alex Caudill
121
+ * Jim Benton
122
+ * John Aughey
123
+ * Keith Pitty
124
+ * Jeff Talbot
125
+ * Dana Contreras
126
+ * Menno van der Sman
127
+ * Bill Harding
128
+ * Isaac Feliu
129
+ * Andrei Bocan
130
+ * László Bácsi
131
+ * Peter Wagenet
132
+ * Max Lapshin
133
+ * Martin Emde
134
+ * David Wennergren
135
+ * Mark Lane
136
+ * Eric Lindvall
137
+ * Lawrence Pit
138
+ * Mike Bailey
139
+ * Bill Leeper
140
+ * Michael Reinsch
141
+ * Anderson Dias
142
+ * Jerome Riga
143
+ * Tien Dung
144
+ * Johannes Kaefer
145
+ * Paul Campbell
146
+ * Matthew Beale
147
+ * Tom Simnett
148
+ * Erik Ostrom
149
+ * Ole Riesenberg
150
+ * Josh Kalderimis
151
+ * J.D. Hollis
152
+ * Jeffrey Chupp
153
+ * Rob Anderton
154
+ * Zach Inglis
155
+ * Ward Bekker
156
+ * Brian Terlson
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 1
3
+ :minor: 2
4
+ :patch: 7
@@ -0,0 +1,210 @@
1
+ Dir[File.join(File.dirname(__FILE__), '../vendor/*/lib')].each do |path|
2
+ $LOAD_PATH << path
3
+ end
4
+
5
+ require 'active_record'
6
+ require 'riddle'
7
+ require 'after_commit'
8
+ require 'yaml'
9
+
10
+ require 'thinking_sphinx/core/string'
11
+ require 'thinking_sphinx/property'
12
+ require 'thinking_sphinx/active_record'
13
+ require 'thinking_sphinx/association'
14
+ require 'thinking_sphinx/attribute'
15
+ require 'thinking_sphinx/configuration'
16
+ require 'thinking_sphinx/excerpter'
17
+ require 'thinking_sphinx/facet'
18
+ require 'thinking_sphinx/class_facet'
19
+ require 'thinking_sphinx/facet_search'
20
+ require 'thinking_sphinx/field'
21
+ require 'thinking_sphinx/index'
22
+ require 'thinking_sphinx/source'
23
+ require 'thinking_sphinx/rails_additions'
24
+ require 'thinking_sphinx/search'
25
+ require 'thinking_sphinx/search_methods'
26
+ require 'thinking_sphinx/deltas'
27
+
28
+ require 'thinking_sphinx/adapters/abstract_adapter'
29
+ require 'thinking_sphinx/adapters/mysql_adapter'
30
+ require 'thinking_sphinx/adapters/postgresql_adapter'
31
+
32
+ ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
33
+
34
+ Merb::Plugins.add_rakefiles(
35
+ File.join(File.dirname(__FILE__), "thinking_sphinx", "tasks")
36
+ ) if defined?(Merb)
37
+
38
+ module ThinkingSphinx
39
+ # A ConnectionError will get thrown when a connection to Sphinx can't be
40
+ # made.
41
+ class ConnectionError < StandardError
42
+ end
43
+
44
+ # A StaleIdsException is thrown by Collection.instances_from_matches if there
45
+ # are records in Sphinx but not in the database, so the search can be retried.
46
+ class StaleIdsException < StandardError
47
+ attr_accessor :ids
48
+ def initialize(ids)
49
+ self.ids = ids
50
+ end
51
+ end
52
+
53
+ # The current version of Thinking Sphinx.
54
+ #
55
+ # @return [String] The version number as a string
56
+ #
57
+ def self.version
58
+ hash = YAML.load_file File.join(File.dirname(__FILE__), '../VERSION.yml')
59
+ [hash[:major], hash[:minor], hash[:patch]].join('.')
60
+ end
61
+
62
+ # The collection of indexed models. Keep in mind that Rails lazily loads
63
+ # its classes, so this may not actually be populated with _all_ the models
64
+ # that have Sphinx indexes.
65
+ def self.indexed_models
66
+ @@indexed_models ||= []
67
+ end
68
+
69
+ def self.unique_id_expression(offset = nil)
70
+ "* #{ThinkingSphinx.indexed_models.size} + #{offset || 0}"
71
+ end
72
+
73
+ # Check if index definition is disabled.
74
+ #
75
+ def self.define_indexes?
76
+ @@define_indexes = true unless defined?(@@define_indexes)
77
+ @@define_indexes == true
78
+ end
79
+
80
+ # Enable/disable indexes - you may want to do this while migrating data.
81
+ #
82
+ # ThinkingSphinx.define_indexes = false
83
+ #
84
+ def self.define_indexes=(value)
85
+ @@define_indexes = value
86
+ end
87
+
88
+ @@deltas_enabled = nil
89
+
90
+ # Check if delta indexing is enabled.
91
+ #
92
+ def self.deltas_enabled?
93
+ @@deltas_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@deltas_enabled.nil?
94
+ @@deltas_enabled
95
+ end
96
+
97
+ # Enable/disable all delta indexing.
98
+ #
99
+ # ThinkingSphinx.deltas_enabled = false
100
+ #
101
+ def self.deltas_enabled=(value)
102
+ @@deltas_enabled = value
103
+ end
104
+
105
+ @@updates_enabled = nil
106
+
107
+ # Check if updates are enabled. True by default, unless within the test
108
+ # environment.
109
+ #
110
+ def self.updates_enabled?
111
+ @@updates_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@updates_enabled.nil?
112
+ @@updates_enabled
113
+ end
114
+
115
+ # Enable/disable updates to Sphinx
116
+ #
117
+ # ThinkingSphinx.updates_enabled = false
118
+ #
119
+ def self.updates_enabled=(value)
120
+ @@updates_enabled = value
121
+ end
122
+
123
+ @@suppress_delta_output = false
124
+
125
+ def self.suppress_delta_output?
126
+ @@suppress_delta_output
127
+ end
128
+
129
+ def self.suppress_delta_output=(value)
130
+ @@suppress_delta_output = value
131
+ end
132
+
133
+ # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
134
+ # or if not using MySQL, this will return false.
135
+ #
136
+ def self.use_group_by_shortcut?
137
+ !!(
138
+ mysql? && ::ActiveRecord::Base.connection.select_all(
139
+ "SELECT @@global.sql_mode, @@session.sql_mode;"
140
+ ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
141
+ )
142
+ end
143
+
144
+ @@remote_sphinx = false
145
+
146
+ # An indication of whether Sphinx is running on a remote machine instead of
147
+ # the same machine.
148
+ #
149
+ def self.remote_sphinx?
150
+ @@remote_sphinx
151
+ end
152
+
153
+ # Tells Thinking Sphinx that Sphinx is running on a different machine, and
154
+ # thus it can't reliably guess whether it is running or not (ie: the
155
+ # #sphinx_running? method), and so just assumes it is.
156
+ #
157
+ # Useful for multi-machine deployments. Set it in your production.rb file.
158
+ #
159
+ # ThinkingSphinx.remote_sphinx = true
160
+ #
161
+ def self.remote_sphinx=(value)
162
+ @@remote_sphinx = value
163
+ end
164
+
165
+ # Check if Sphinx is running. If remote_sphinx is set to true (indicating
166
+ # Sphinx is on a different machine), this will always return true, and you
167
+ # will have to handle any connection errors yourself.
168
+ #
169
+ def self.sphinx_running?
170
+ remote_sphinx? || sphinx_running_by_pid?
171
+ end
172
+
173
+ # Check if Sphinx is actually running, provided the pid is on the same
174
+ # machine as this code.
175
+ #
176
+ def self.sphinx_running_by_pid?
177
+ !!sphinx_pid && pid_active?(sphinx_pid)
178
+ end
179
+
180
+ def self.sphinx_pid
181
+ if File.exists?(ThinkingSphinx::Configuration.instance.pid_file)
182
+ File.read(ThinkingSphinx::Configuration.instance.pid_file)[/\d+/]
183
+ else
184
+ nil
185
+ end
186
+ end
187
+
188
+ def self.pid_active?(pid)
189
+ !!Process.kill(0, pid.to_i)
190
+ rescue Exception => e
191
+ false
192
+ end
193
+
194
+ def self.microsoft?
195
+ RUBY_PLATFORM =~ /mswin/
196
+ end
197
+
198
+ def self.jruby?
199
+ defined?(JRUBY_VERSION)
200
+ end
201
+
202
+ def self.mysql?
203
+ ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlAdapter" ||
204
+ ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlplusAdapter" || (
205
+ jruby? && ::ActiveRecord::Base.connection.config[:adapter] == "jdbcmysql"
206
+ )
207
+ end
208
+
209
+ extend ThinkingSphinx::SearchMethods::ClassMethods
210
+ end
@@ -0,0 +1,298 @@
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
+ class << self
16
+
17
+ def set_sphinx_primary_key(attribute)
18
+ @sphinx_primary_key_attribute = attribute
19
+ end
20
+
21
+ def primary_key_for_sphinx
22
+ @sphinx_primary_key_attribute || primary_key
23
+ end
24
+
25
+ # Allows creation of indexes for Sphinx. If you don't do this, there
26
+ # isn't much point trying to search (or using this plugin at all,
27
+ # really).
28
+ #
29
+ # An example or two:
30
+ #
31
+ # define_index
32
+ # indexes :id, :as => :model_id
33
+ # indexes name
34
+ # end
35
+ #
36
+ # You can also grab fields from associations - multiple levels deep
37
+ # if necessary.
38
+ #
39
+ # define_index do
40
+ # indexes tags.name, :as => :tag
41
+ # indexes articles.content
42
+ # indexes orders.line_items.product.name, :as => :product
43
+ # end
44
+ #
45
+ # And it will automatically concatenate multiple fields:
46
+ #
47
+ # define_index do
48
+ # indexes [author.first_name, author.last_name], :as => :author
49
+ # end
50
+ #
51
+ # The #indexes method is for fields - if you want attributes, use
52
+ # #has instead. All the same rules apply - but keep in mind that
53
+ # attributes are for sorting, grouping and filtering, not searching.
54
+ #
55
+ # define_index do
56
+ # # fields ...
57
+ #
58
+ # has created_at, updated_at
59
+ # end
60
+ #
61
+ # One last feature is the delta index. This requires the model to
62
+ # have a boolean field named 'delta', and is enabled as follows:
63
+ #
64
+ # define_index do
65
+ # # fields ...
66
+ # # attributes ...
67
+ #
68
+ # set_property :delta => true
69
+ # end
70
+ #
71
+ # Check out the more detailed documentation for each of these methods
72
+ # at ThinkingSphinx::Index::Builder.
73
+ #
74
+ def define_index(&block)
75
+ return unless ThinkingSphinx.define_indexes?
76
+
77
+ self.sphinx_indexes ||= []
78
+ index = ThinkingSphinx::Index::Builder.generate(self, &block)
79
+
80
+ self.sphinx_indexes << index
81
+ unless ThinkingSphinx.indexed_models.include?(self.name)
82
+ ThinkingSphinx.indexed_models << self.name
83
+ end
84
+
85
+ if index.delta?
86
+ before_save :toggle_delta
87
+ after_commit :index_delta
88
+ end
89
+
90
+ after_destroy :toggle_deleted
91
+
92
+ include ThinkingSphinx::SearchMethods
93
+ include ThinkingSphinx::ActiveRecord::AttributeUpdates
94
+ include ThinkingSphinx::ActiveRecord::Scopes
95
+
96
+ index
97
+
98
+ # We want to make sure that if the database doesn't exist, then Thinking
99
+ # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
100
+ # and db:migrate). It's a bit hacky, but I can't think of a better way.
101
+ rescue StandardError => err
102
+ case err.class.name
103
+ when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
104
+ return
105
+ else
106
+ raise err
107
+ end
108
+ end
109
+ alias_method :sphinx_index, :define_index
110
+
111
+ def sphinx_index_options
112
+ sphinx_indexes.last.options
113
+ end
114
+
115
+ # Generate a unique CRC value for the model's name, to use to
116
+ # determine which Sphinx documents belong to which AR records.
117
+ #
118
+ # Really only written for internal use - but hey, if it's useful to
119
+ # you in some other way, awesome.
120
+ #
121
+ def to_crc32
122
+ self.name.to_crc32
123
+ end
124
+
125
+ def to_crc32s
126
+ (subclasses << self).collect { |klass| klass.to_crc32 }
127
+ end
128
+
129
+ def source_of_sphinx_index
130
+ possible_models = self.sphinx_indexes.collect { |index| index.model }
131
+ return self if possible_models.include?(self)
132
+
133
+ parent = self.superclass
134
+ while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
135
+ parent = parent.superclass
136
+ end
137
+
138
+ return parent
139
+ end
140
+
141
+ def to_riddle(offset)
142
+ sphinx_database_adapter.setup
143
+
144
+ indexes = [to_riddle_for_core(offset)]
145
+ indexes << to_riddle_for_delta(offset) if sphinx_delta?
146
+ indexes << to_riddle_for_distributed
147
+ end
148
+
149
+ def sphinx_database_adapter
150
+ @sphinx_database_adapter ||=
151
+ ThinkingSphinx::AbstractAdapter.detect(self)
152
+ end
153
+
154
+ def sphinx_name
155
+ self.name.underscore.tr(':/\\', '_')
156
+ end
157
+
158
+ private
159
+
160
+ def sphinx_delta?
161
+ self.sphinx_indexes.any? { |index| index.delta? }
162
+ end
163
+
164
+ def to_riddle_for_core(offset)
165
+ index = Riddle::Configuration::Index.new("#{sphinx_name}_core")
166
+ index.path = File.join(
167
+ ThinkingSphinx::Configuration.instance.searchd_file_path, index.name
168
+ )
169
+
170
+ set_configuration_options_for_indexes index
171
+ set_field_settings_for_indexes index
172
+
173
+ self.sphinx_indexes.select { |ts_index|
174
+ ts_index.model == self
175
+ }.each_with_index do |ts_index, i|
176
+ index.sources += ts_index.sources.collect { |source|
177
+ source.to_riddle_for_core(offset, i)
178
+ }
179
+ end
180
+
181
+ index
182
+ end
183
+
184
+ def to_riddle_for_delta(offset)
185
+ index = Riddle::Configuration::Index.new("#{sphinx_name}_delta")
186
+ index.parent = "#{sphinx_name}_core"
187
+ index.path = File.join(ThinkingSphinx::Configuration.instance.searchd_file_path, index.name)
188
+
189
+ self.sphinx_indexes.each_with_index do |ts_index, i|
190
+ index.sources += ts_index.sources.collect { |source|
191
+ source.to_riddle_for_delta(offset, i)
192
+ } if ts_index.delta?
193
+ end
194
+
195
+ index
196
+ end
197
+
198
+ def to_riddle_for_distributed
199
+ index = Riddle::Configuration::DistributedIndex.new(sphinx_name)
200
+ index.local_indexes << "#{sphinx_name}_core"
201
+ index.local_indexes.unshift "#{sphinx_name}_delta" if sphinx_delta?
202
+ index
203
+ end
204
+
205
+ def set_configuration_options_for_indexes(index)
206
+ ThinkingSphinx::Configuration.instance.index_options.each do |key, value|
207
+ index.send("#{key}=".to_sym, value)
208
+ end
209
+
210
+ self.sphinx_indexes.each do |ts_index|
211
+ ts_index.options.each do |key, value|
212
+ index.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) && !value.nil?
213
+ end
214
+ end
215
+ end
216
+
217
+ def set_field_settings_for_indexes(index)
218
+ field_names = lambda { |field| field.unique_name.to_s }
219
+
220
+ self.sphinx_indexes.each do |ts_index|
221
+ index.prefix_field_names += ts_index.prefix_fields.collect(&field_names)
222
+ index.infix_field_names += ts_index.infix_fields.collect(&field_names)
223
+ end
224
+ end
225
+ end
226
+ end
227
+
228
+ base.send(:include, ThinkingSphinx::ActiveRecord::Delta)
229
+
230
+ ::ActiveRecord::Associations::HasManyAssociation.send(
231
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
232
+ )
233
+ ::ActiveRecord::Associations::HasManyThroughAssociation.send(
234
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
235
+ )
236
+ end
237
+
238
+ def in_index?(suffix)
239
+ self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
240
+ end
241
+
242
+ def in_core_index?
243
+ in_index? "core"
244
+ end
245
+
246
+ def in_delta_index?
247
+ in_index? "delta"
248
+ end
249
+
250
+ def in_both_indexes?
251
+ in_core_index? && in_delta_index?
252
+ end
253
+
254
+ def toggle_deleted
255
+ return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running?
256
+
257
+ config = ThinkingSphinx::Configuration.instance
258
+ client = Riddle::Client.new config.address, config.port
259
+
260
+ client.update(
261
+ "#{self.class.sphinx_indexes.first.name}_core",
262
+ ['sphinx_deleted'],
263
+ {self.sphinx_document_id => 1}
264
+ ) if self.in_core_index?
265
+
266
+ client.update(
267
+ "#{self.class.sphinx_indexes.first.name}_delta",
268
+ ['sphinx_deleted'],
269
+ {self.sphinx_document_id => 1}
270
+ ) if ThinkingSphinx.deltas_enabled? &&
271
+ self.class.sphinx_indexes.any? { |index| index.delta? } &&
272
+ self.toggled_delta?
273
+ rescue ::ThinkingSphinx::ConnectionError
274
+ # nothing
275
+ end
276
+
277
+ # Returns the unique integer id for the object. This method uses the
278
+ # attribute hash to get around ActiveRecord always mapping the #id method
279
+ # to whatever the real primary key is (which may be a unique string hash).
280
+ #
281
+ # @return [Integer] Unique record id for the purposes of Sphinx.
282
+ #
283
+ def primary_key_for_sphinx
284
+ read_attribute(self.class.primary_key_for_sphinx)
285
+ end
286
+
287
+ def sphinx_document_id
288
+ primary_key_for_sphinx * ThinkingSphinx.indexed_models.size +
289
+ ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name)
290
+ end
291
+
292
+ private
293
+
294
+ def sphinx_index_name(suffix)
295
+ "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
296
+ end
297
+ end
298
+ end