airblade-Sphincter 1.1.0

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.
data/History.txt ADDED
@@ -0,0 +1,15 @@
1
+ == 1.1.0 / 2007-08-13
2
+
3
+ * 2 major enhancements:
4
+ * Fields across relationships may be included via add_index.
5
+ * Sphincter now automatically configures Dmytro Shteflyuk's sphinx API. Run
6
+ `rake sphincter:setup_sphinx` and check in vendor/plugins/sphinx.
7
+ * 1 bug fix:
8
+ * `rake sphincter:index` task didn't correctly run reindex. Bug submitted
9
+ by Lee O'Mara.
10
+
11
+ == 1.0.0 / 2007-07-26
12
+
13
+ * 1 major enhancement:
14
+ * Birthday!
15
+
data/LICENSE.txt ADDED
@@ -0,0 +1,27 @@
1
+ Copyright 2007 Eric Hodel. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions
5
+ are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+ 3. Neither the names of the authors nor the names of their contributors
13
+ may be used to endorse or promote products derived from this software
14
+ without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
17
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
20
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
21
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
22
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
23
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
25
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
26
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+
data/Manifest.txt ADDED
@@ -0,0 +1,16 @@
1
+ History.txt
2
+ LICENSE.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/sphincter.rb
7
+ lib/sphincter/association_searcher.rb
8
+ lib/sphincter/configure.rb
9
+ lib/sphincter/search.rb
10
+ lib/sphincter/search_stub.rb
11
+ lib/sphincter/tasks.rb
12
+ test/sphincter_test_case.rb
13
+ test/test_sphincter_association_searcher.rb
14
+ test/test_sphincter_configure.rb
15
+ test/test_sphincter_search.rb
16
+ test/test_sphincter_search_stub.rb
data/README.txt ADDED
@@ -0,0 +1,153 @@
1
+ Sphincter
2
+
3
+ Eric Hodel <drbrain@segment7.net>
4
+
5
+ http://seattlerb.org/Sphincter
6
+
7
+ File bugs:
8
+
9
+ http://rubyforge.org/tracker/?func=add&group_id=1513&atid=5921
10
+
11
+ Sphincter was named by David Yeu.
12
+
13
+ == DESCRIPTION:
14
+
15
+ Sphincter is an ActiveRecord extension for full-text searching with Sphinx.
16
+
17
+ Sphincter uses Dmytro Shteflyuk's sphinx Ruby API and automatic
18
+ configuration to make totally rad ActiveRecord searching. Well, you
19
+ still have to tell Sphincter what models you want to search. It
20
+ doesn't read your mind.
21
+
22
+ For complete documentation:
23
+
24
+ ri Sphincter
25
+
26
+ == FEATURES:
27
+
28
+ * Automatically configures itself.
29
+ * Handy set of rake tasks for easy, automatic management.
30
+ * Automatically adds has_many metadata for searching across the
31
+ association.
32
+ * Stub for testing without connecting to searchd, Sphincter::SearchStub.
33
+ * Easy pagination support.
34
+ * Filtering by index metadata and ranges, including dates.
35
+
36
+ == PROBLEMS:
37
+
38
+ * Setting match mode not supported.
39
+ * Setting sort mode not supported.
40
+ * Setting per-field weights not supported.
41
+ * Setting id range not supported.
42
+ * Setting group-by not supported.
43
+
44
+ == QUICK-START:
45
+
46
+ Download and install Sphinx from http://www.sphinxsearch.com/downloads.html
47
+
48
+ Install Sphincter:
49
+
50
+ $ gem install Sphincter
51
+
52
+ Load Sphincter tasks in Rakefile:
53
+
54
+ require 'sphincter/tasks'
55
+
56
+ Setup the Dmytro Shteflyuk's Sphinx client:
57
+
58
+ $ rake sphincter:setup_sphinx
59
+
60
+ Add vendor/plugins/sphinx to your SCM system.
61
+
62
+ Load Sphincter in config/environment.rb:
63
+
64
+ require 'sphincter'
65
+
66
+ Add indexes to models:
67
+
68
+ class Post < ActiveRecord::Base
69
+ belongs_to :blog
70
+ add_index :fields => %w[title body published]
71
+ end
72
+
73
+ Add searching UI:
74
+
75
+ class BlogController < ApplicationController
76
+ def search
77
+ @blog = Blog.find params[:id]
78
+
79
+ @results = @blog.posts.search params[:q]
80
+ end
81
+ end
82
+
83
+ <ol>
84
+ <% @results.records.each do |post| -%>
85
+ <li>
86
+ <div><%= link_to post.title, post_path(post) %></div>
87
+ <div><%= truncate post.body, 250 %></div>
88
+ </li>
89
+ <% end -%>
90
+ </ol>
91
+
92
+ Start searchd:
93
+
94
+ $ rake sphincter:start_searchd
95
+
96
+ Then test it out in your browser.
97
+
98
+ NOTE: By default, Sphincter will run searchd on the same port for all
99
+ environments. See Sphincter::Configure for how to configure different
100
+ environments to use different ports.
101
+
102
+ == TESTING QUICK-START:
103
+
104
+ See Sphinx::SearchStub.
105
+
106
+ == EXAMPLES:
107
+
108
+ See Sphincter::Search#search for full documentation.
109
+
110
+ Example ActiveRecord model:
111
+
112
+ class Post < ActiveRecord::Base
113
+ belongs_to :blog
114
+ belongs_to :user
115
+
116
+ # published is a boolean and title and body are string or text fields
117
+ # user.name is automatically fetched via the user association
118
+ add_index :fields => %w[title body published]
119
+ end
120
+
121
+ Simple search:
122
+
123
+ Post.search 'words'
124
+
125
+ Only search published posts:
126
+
127
+ Post.search 'words', :conditions => { :published => 1 }
128
+
129
+ Only search posts created in the last week:
130
+
131
+ now = Time.now
132
+ ago = now - 1.weeks
133
+ Post.search 'words', :between => { :created_on => [ago, now] }
134
+
135
+ Pagination (defaults to ten records/page):
136
+
137
+ Post.search 'words', :page => 2
138
+
139
+ Pagination with custom page size:
140
+
141
+ Post.search 'words', :page => 2, :per_page => 20
142
+
143
+ Pagination with custom page size (better):
144
+
145
+ Add to config/sphincter.yml:
146
+
147
+ sphincter:
148
+ per_page: 20
149
+
150
+ Then search:
151
+
152
+ Post.search 'words', :page => 2
153
+
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ $:.unshift 'lib'
6
+ require 'sphincter'
7
+
8
+ Hoe.new('Sphincter', Sphincter::VERSION) do |p|
9
+ p.rubyforge_name = 'seattlerb'
10
+ p.author = 'Eric Hodel'
11
+ p.email = 'drbrain@segment7.net'
12
+ p.summary = p.paragraphs_of('README.txt', 7).first
13
+ p.description = p.paragraphs_of('README.txt', 8).first
14
+ p.url = p.paragraphs_of('README.txt', 2).first
15
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
16
+
17
+ p.extra_deps << ['rake', '>= 0.7.3']
18
+ p.extra_deps << ['rails', '>= 1.2.3']
19
+ end
20
+
21
+ # vim: syntax=Ruby
data/lib/sphincter.rb ADDED
@@ -0,0 +1,107 @@
1
+ $TESTING = defined?($TESTING) && $TESTING
2
+
3
+ ##
4
+ # Sphincter is a ActiveRecord extension for full-text searching using the
5
+ # Sphinx library.
6
+ #
7
+ # For the quick-start guide and some examples, see README.txt.
8
+ #
9
+ # == Installing
10
+ #
11
+ # Download and install Sphinx from http://www.sphinxsearch.com/downloads.html
12
+ #
13
+ # Download Sphinx Ruby API from
14
+ # http://rubyforge.org/frs/?group_id=2604&release_id=11049
15
+ #
16
+ # Unpack Sphinx Ruby API into vendor/plugins/.
17
+ #
18
+ # Install the gem:
19
+ #
20
+ # gem install Sphincter
21
+ #
22
+ # Require Sphincter in config/environment.rb:
23
+ #
24
+ # require 'sphincter'
25
+ #
26
+ # Require the Sphincter rake tasks in Rakefile:
27
+ #
28
+ # require 'sphincter/tasks'
29
+ #
30
+ # == Setup
31
+ #
32
+ # At best, you don't do anything to setup Sphincter. It has sensible built-in
33
+ # defaults.
34
+ #
35
+ # If you're running Sphinx's searchd for multiple environments on the same
36
+ # machine, you'll want to add a config file to change the port that searchd
37
+ # and the RAILS_ENV will comminicate across. Do that in a per-environment
38
+ # configuration file.
39
+ #
40
+ # If you have multiple machines, you'll want to change which address searchd
41
+ # will run on. Do that in the global configuration file.
42
+ #
43
+ # See Sphincter::Configure for full information on how to setup these and
44
+ # other options for Sphincter.
45
+ #
46
+ # When you're done, run:
47
+ #
48
+ # $ rake sphincter:configure
49
+ #
50
+ # == Indexing
51
+ #
52
+ # Sphincter automatically extends ActiveRecord::Base with Sphincter::Search, so
53
+ # you only have to call add_index in the models you want indexed:
54
+ #
55
+ # class Model < ActiveRecord::Base
56
+ # belongs_to :other
57
+ #
58
+ # add_index :fields => %w[title body]
59
+ # end
60
+ #
61
+ # class Other < ActiveRecord::Base
62
+ # has_many :models
63
+ # end
64
+ #
65
+ # add_index automatically adds a #search method to has_many associations
66
+ # referencing this model, so you could:
67
+ #
68
+ # Other.find(id).models.search 'some query'
69
+ #
70
+ # See Sphincter::Search for details.
71
+ #
72
+ # When you're done, run:
73
+ #
74
+ # rake sphincter:index
75
+ #
76
+ # == Tasks
77
+ #
78
+ # You can get a set of Sphincter tasks by requiring 'sphincter/tasks' in your
79
+ # Rakefile. These tasks are all in the 'sphincter' namespace:
80
+ #
81
+ # configure:: Creates sphinx.conf if it doesn't exist
82
+ # reconfigure:: Creates sphinx.conf, replacing the existing one.
83
+ # index:: Runs the sphinx indexer if the index doesn't exist.
84
+ # reindex:: Runs the sphinx indexer. Rotates the index if searchd is running.
85
+ # reset:: Stops searchd, reconfigures and reindexes
86
+ # restart_searchd:: Restarts the searchd sphinx daemon
87
+ # start_searchd:: Starts the searchd sphinx daemon
88
+ # stop_searchd:: Stops the searchd daemon
89
+
90
+ module Sphincter
91
+
92
+ ##
93
+ # This is the version of Sphincter you are using.
94
+
95
+ VERSION = '1.1.0'
96
+
97
+ ##
98
+ # Sphincter error base class.
99
+
100
+ class Error < RuntimeError; end
101
+
102
+ end
103
+
104
+ require 'sphincter/configure'
105
+ require 'sphincter/association_searcher'
106
+ require 'sphincter/search'
107
+
@@ -0,0 +1,22 @@
1
+ require 'sphincter'
2
+
3
+ ##
4
+ # ActiveRecord::Associations::ClassMethods#has_many extension for searching
5
+ # the items of an ActiveRecord::Associations::AssociationProxy.
6
+
7
+ module Sphincter::AssociationSearcher
8
+
9
+ ##
10
+ # Searches for +query+ with +options+. Adds a condition so only the
11
+ # proxy_owner's records are matched.
12
+
13
+ def search(query, options = {})
14
+ pkey = proxy_reflection.primary_key_name
15
+ options[:conditions] ||= {}
16
+ options[:conditions][pkey] = proxy_owner.id
17
+
18
+ proxy_reflection.klass.search query, options
19
+ end
20
+
21
+ end
22
+
@@ -0,0 +1,499 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ require 'sphincter'
5
+
6
+ ##
7
+ # Configuration module for Sphincter.
8
+ #
9
+ # DEFAULT_CONF contains the default options. They can be overridden in both a
10
+ # global config/sphincter.yml and in a per-environment
11
+ # config/environments/sphincter.RAILS_ENV.yml.
12
+ #
13
+ # The only option you should need to override is the port option of
14
+ # sphincter, so a config file for separate test and development indexes would
15
+ # look like:
16
+ #
17
+ # config/environments/sphincter.development.yml:
18
+ #
19
+ # sphincter:
20
+ # port: 3313
21
+ #
22
+ # config/environments/sphincter.test.yml:
23
+ #
24
+ # sphincter:
25
+ # port: 3314
26
+ #
27
+ # Configuration options:
28
+ #
29
+ # sphincter:: Options for serachd's and Sphinx's port and address, and
30
+ # paths for index files.
31
+ # index:: Options for a sphinx index conf section
32
+ # indexer:: Options for the sphinx indexer
33
+ # mysql:: Options for the sphinx indexer's mysql database connection. The
34
+ # important ones are filled from config/database.yml
35
+ # searchd:: Options for a sphinx searchd conf section
36
+ # source:: Options for a sphinx source conf section
37
+ #
38
+ # The sphincter entry contains:
39
+ #
40
+ # address:: Which host searchd will run on, and which host Sphincter will
41
+ # connect to.
42
+ # port:: Which port searchd and Sphincter will connect to.
43
+ # path:: Location of searchd indexes, relative to RAILS_ROOT.
44
+ # per_page:: How many items to include in a search by default.
45
+ #
46
+ # All other entries are from Sphinx.
47
+ #
48
+ # See http://www.sphinxsearch.com/doc.html#reference for details on sphinx
49
+ # conf file settings.
50
+
51
+ module Sphincter::Configure
52
+
53
+ ##
54
+ # A class for building sphinx.conf source/index sections.
55
+
56
+ class Index
57
+
58
+ attr_reader :source_conf
59
+
60
+ attr_reader :name
61
+
62
+ ##
63
+ # Creates a new Index for +klass+ and +options+.
64
+
65
+ def initialize(klass, options)
66
+ @fields = []
67
+ @where = []
68
+ @group = false
69
+
70
+ @source_conf = {}
71
+ @source_conf['sql_date_column'] = []
72
+ @source_conf['sql_group_column'] = %w[sphincter_index_id]
73
+
74
+ @klass = klass
75
+ @table = @klass.table_name
76
+ @conn = @klass.connection
77
+ @tables = @table.dup
78
+
79
+ defaults = {
80
+ :conditions => [],
81
+ :fields => [],
82
+ :name => @table,
83
+ }
84
+
85
+ @options = defaults.merge options
86
+
87
+ @name = @options[:name] || @table
88
+ end
89
+
90
+ ##
91
+ # Adds plain field +field+ to the index from class +klass+ using
92
+ # +as_table+ as the table name.
93
+
94
+ def add_field(field, klass = @klass, as_table = nil)
95
+ table = klass.table_name
96
+ quoted_field = @conn.quote_column_name field
97
+
98
+ column_type = klass.columns_hash[field].type
99
+ expr = case column_type
100
+ when :date, :datetime, :time, :timestamp then
101
+ @source_conf['sql_date_column'] << field
102
+ "UNIX_TIMESTAMP(#{table}.#{quoted_field})"
103
+ when :boolean, :integer then
104
+ @source_conf['sql_group_column'] << field
105
+ "#{table}.#{quoted_field}"
106
+ when :string, :text then
107
+ "#{table}.#{quoted_field}"
108
+ else
109
+ raise Sphincter::Error, "unknown column type #{column_type}"
110
+ end
111
+
112
+ as_name = [as_table, field].compact.join '_'
113
+ as_name = @conn.quote_column_name as_name
114
+
115
+ "#{expr} AS #{as_name}"
116
+ end
117
+
118
+ ##
119
+ # Includes field +as_field+ from association +as_name+ in the index.
120
+
121
+ def add_include(as_name, as_field)
122
+ as_assoc = @klass.reflect_on_all_associations.find do |assoc|
123
+ assoc.name == as_name.intern
124
+ end
125
+
126
+ if as_assoc.nil? then
127
+ raise Sphincter::Error,
128
+ "could not find association \"#{as_name}\" in #{@klass.name}"
129
+ end
130
+
131
+ as_klass = as_assoc.class_name.constantize
132
+ as_table = as_klass.table_name
133
+
134
+ as_klass_key = @conn.quote_column_name as_klass.primary_key.to_s
135
+ as_assoc_key = @conn.quote_column_name as_assoc.primary_key_name.to_s
136
+
137
+ case as_assoc.macro
138
+ when :belongs_to then
139
+ @fields << add_field(as_field, as_klass, as_table)
140
+ @tables << " LEFT JOIN #{as_table} ON" \
141
+ " #{@table}.#{as_assoc_key} = #{as_table}.#{as_klass_key}"
142
+
143
+ when :has_many then
144
+ if as_assoc.options.include? :through then
145
+ raise Sphincter::Error,
146
+ "unsupported macro has_many :through for \"#{as_name}\" " \
147
+ "in #{klass.name}.add_index"
148
+ end
149
+
150
+ as_pkey = @conn.quote_column_name as_klass.primary_key.to_s
151
+ as_fkey = @conn.quote_column_name as_assoc.primary_key_name.to_s
152
+
153
+ as_name = [as_table, as_field].compact.join '_'
154
+ as_name = @conn.quote_column_name as_name
155
+
156
+ field = @conn.quote_column_name as_field
157
+
158
+ @fields << "GROUP_CONCAT(#{as_table}.#{field} SEPARATOR ' ') AS #{as_name}"
159
+
160
+ if as_assoc.options.include? :as then
161
+ poly_name = as_assoc.options[:as]
162
+ id_col = @conn.quote_column_name "#{poly_name}_id"
163
+ type_col = @conn.quote_column_name "#{poly_name}_type"
164
+
165
+ @tables << " LEFT JOIN #{as_table} ON"\
166
+ " #{@table}.#{as_klass_key} = #{as_table}.#{id_col} AND" \
167
+ " #{@conn.quote @klass.name} = #{as_table}.#{type_col}"
168
+ else
169
+ @tables << " LEFT JOIN #{as_table} ON" \
170
+ " #{@table}.#{as_klass_key} = #{as_table}.#{as_assoc_key}"
171
+ end
172
+
173
+ @group = true
174
+ else
175
+ raise Sphincter::Error,
176
+ "unsupported macro #{as_assoc.macro} for \"#{as_name}\" " \
177
+ "in #{klass.name}.add_index"
178
+ end
179
+ end
180
+
181
+ def configure
182
+ conn = @klass.connection
183
+ pk = conn.quote_column_name @klass.primary_key
184
+ index_id = @options[:index_id]
185
+
186
+ index_count = Sphincter::Configure.index_count
187
+
188
+ @fields << "(#{@table}.#{pk} * #{index_count} + #{index_id}) AS #{pk}"
189
+ @fields << "#{index_id} AS sphincter_index_id"
190
+ @fields << "'#{@klass.name}' AS sphincter_klass"
191
+
192
+ @options[:fields].each do |field|
193
+ case field
194
+ when /\./ then add_include(*field.split('.', 2))
195
+ else @fields << add_field(field)
196
+ end
197
+ end
198
+
199
+ @fields = @fields.join ', '
200
+
201
+ @where << "#{@table}.#{pk} >= $start"
202
+ @where << "#{@table}.#{pk} <= $end"
203
+ @where.push(*@options[:conditions])
204
+ @where = @where.compact.join ' AND '
205
+
206
+ query = "SELECT #{@fields} FROM #{@tables} WHERE #{@where}"
207
+ query << " GROUP BY #{@table}.#{pk}" if @group
208
+
209
+ @source_conf['sql_query'] = query
210
+ @source_conf['sql_query_info'] =
211
+ "SELECT * FROM #{@table} " \
212
+ "WHERE #{@table}.#{pk} = (($id - #{index_id}) / #{index_count})"
213
+ @source_conf['sql_query_range'] =
214
+ "SELECT MIN(#{pk}), MAX(#{pk}) FROM #{@table}"
215
+ @source_conf['strip_html'] = @options[:strip_html] ? 1 : 0
216
+
217
+ @source_conf
218
+ end
219
+
220
+ end
221
+
222
+ @env_conf = nil
223
+ @index_count = nil
224
+
225
+ rails_env = defined?(RAILS_ENV) ? RAILS_ENV : 'RAILS_ENV'
226
+
227
+ ##
228
+ # Default Sphincter configuration.
229
+
230
+ DEFAULT_CONF = {
231
+ 'sphincter' => {
232
+ 'address' => '127.0.0.1',
233
+ 'path' => "sphinx/#{rails_env}",
234
+ 'per_page' => 10,
235
+ 'port' => 3312,
236
+ },
237
+
238
+ 'index' => {
239
+ 'charset_type' => 'utf-8',
240
+ 'docinfo' => 'extern',
241
+ 'min_word_len' => 1,
242
+ 'morphology' => 'stem_en',
243
+ 'stopwords' => '',
244
+ },
245
+
246
+ 'indexer' => {
247
+ 'mem_limit' => '32M',
248
+ },
249
+
250
+ 'mysql' => {
251
+ 'sql_query_pre' => [
252
+ 'SET NAMES utf8',
253
+ ],
254
+ },
255
+
256
+ 'searchd' => {
257
+ 'log' => "log/sphinx/searchd.#{rails_env}.log",
258
+ 'max_children' => 30,
259
+ 'max_matches' => 1000,
260
+ 'query_log' => "log/sphinx/query.#{rails_env}.log",
261
+ 'read_timeout' => 5,
262
+ },
263
+
264
+ 'source' => {
265
+ 'index_html_attrs' => '',
266
+ 'sql_query_post' => '',
267
+ 'sql_range_step' => 20000,
268
+ 'strip_html' => 0,
269
+ },
270
+ }
271
+
272
+ ##
273
+ # Builds and writes out a sphinx.conf file.
274
+
275
+ def self.configure
276
+ conf = get_conf
277
+ db_conf = get_db_conf
278
+
279
+ db_conf = conf[db_conf['type']].merge db_conf
280
+
281
+ sources = get_sources
282
+
283
+ sources.each do |name, source_conf|
284
+ sources[name] = db_conf.merge source_conf
285
+ end
286
+
287
+ write_configuration conf, sources
288
+ end
289
+
290
+ ##
291
+ # Merges Hashes of Hashes +mergee+ and +hash+.
292
+
293
+ def self.deep_merge(mergee, hash)
294
+ mergee = mergee.dup
295
+ hash.keys.each do |key| mergee[key] ||= hash[key] end
296
+ mergee.each do |key, value|
297
+ next unless hash[key]
298
+ mergee[key] = value.merge hash[key]
299
+ end
300
+ end
301
+
302
+ ##
303
+ # Builds the Sphincter configuration.
304
+ #
305
+ # Automatically fills in searchd address, port and pid_file from 'sphincter'
306
+ # section.
307
+
308
+ def self.get_conf
309
+ return @env_conf unless @env_conf.nil?
310
+
311
+ base_file = File.expand_path File.join(RAILS_ROOT, 'config', 'sphincter.yml')
312
+ base_conf = deep_merge DEFAULT_CONF, get_conf_from(base_file)
313
+
314
+ env_file = File.expand_path File.join(RAILS_ROOT, 'config', 'environments',
315
+ "sphincter.#{RAILS_ENV}.yml")
316
+ env_conf = deep_merge base_conf, get_conf_from(env_file)
317
+
318
+ env_conf['searchd']['address'] = env_conf['sphincter']['address']
319
+ env_conf['searchd']['port'] = env_conf['sphincter']['port']
320
+ env_conf['searchd']['pid_file'] = File.join(env_conf['sphincter']['path'],
321
+ 'searchd.pid')
322
+
323
+ @env_conf = env_conf
324
+ end
325
+
326
+ ##
327
+ # Reads configuration file +file+. Returns {} if the file does not exist.
328
+
329
+ def self.get_conf_from(file)
330
+ if File.exist? file then
331
+ YAML.load File.read(file)
332
+ else
333
+ {}
334
+ end
335
+ end
336
+
337
+ ##
338
+ # Builds a sphinx.conf source configuration for each index.
339
+
340
+ def self.get_sources
341
+ load_models
342
+
343
+ indexes = Sphincter::Search.indexes
344
+ index_count # HACK necessary to set options[:index_id] per-index
345
+
346
+ sources = {}
347
+
348
+ indexes.each do |klass, model_indexes|
349
+ model_indexes.each do |options|
350
+ index = Index.new klass, options
351
+ index.configure
352
+
353
+ sources[index.name] = index.source_conf
354
+ end
355
+ end
356
+
357
+ sources
358
+ end
359
+
360
+ ##
361
+ # Builds a field for a source's sql_query sphinx.conf setting.
362
+ #
363
+ # get_sources_field only understands :datetime, :boolean, :integer, :string
364
+ # and :text column types.
365
+
366
+ ##
367
+ # Retrieves the database configuration for ActiveRecord::Base and adapts it
368
+ # for a sphinx.conf file.
369
+
370
+ def self.get_db_conf
371
+ conf = {}
372
+ ar_conf = ActiveRecord::Base.configurations[::RAILS_ENV]
373
+
374
+ conf['type'] = ar_conf['adapter']
375
+ conf['sql_host'] = ar_conf['host'] if ar_conf.include? 'host'
376
+ conf['sql_user'] = ar_conf['username'] if ar_conf.include? 'username'
377
+ conf['sql_pass'] = ar_conf['password'] if ar_conf.include? 'password'
378
+ conf['sql_db'] = ar_conf['database'] if ar_conf.include? 'database'
379
+ conf['sql_sock'] = ar_conf['socket'] if ar_conf.include? 'socket'
380
+
381
+ conf
382
+ end
383
+
384
+ ##
385
+ # Iterates over the searchable ActiveRecord::Base classes and assigns an
386
+ # index to each one. Returns the total number of indexes found.
387
+
388
+ def self.index_count
389
+ return @index_count unless @index_count.nil?
390
+
391
+ @index_count = 0
392
+
393
+ load_models
394
+
395
+ Sphincter::Search.indexes.each do |model, model_indexes|
396
+ model_indexes.each do |options|
397
+ options[:index_id] = @index_count
398
+ @index_count += 1
399
+ end
400
+ end
401
+ @index_count
402
+ end
403
+
404
+ ##
405
+ # Loads ActiveRecord::Base models from app/models.
406
+
407
+ def self.load_models
408
+ model_files = Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')]
409
+ model_names = model_files.map { |name| File.basename name, '.rb' }
410
+ model_names.each { |name| name.camelize.constantize }
411
+ end
412
+
413
+ ##
414
+ # Returns the pid of searchd if searchd is running, otherwise false.
415
+
416
+ def self.searchd_running?
417
+ pid_file = Sphincter::Configure.get_conf['searchd']['pid_file']
418
+ return false unless File.exist? pid_file
419
+
420
+ pid = File.read(pid_file).chomp
421
+ return false if pid.empty?
422
+
423
+ running = `ps -p #{pid}` =~ /#{pid}.*searchd/
424
+ running ? pid : false
425
+ end
426
+
427
+ ##
428
+ # Outputs a sphinx.conf configuration section titled +heading+ using the
429
+ # Hash +data+. Values in +data+ may be a String or Array. For an Array,
430
+ # the Hash key is printed multiple times.
431
+
432
+ def self.section(heading, data)
433
+ section = []
434
+ section << heading
435
+ section << '{'
436
+ data.sort_by { |k,| k }.each do |key, value|
437
+ case value
438
+ when Array then
439
+ next if value.empty?
440
+ value.each do |v|
441
+ section << " #{key} = #{v}"
442
+ end
443
+ else
444
+ section << " #{key} = #{value}"
445
+ end
446
+ end
447
+ section << '}'
448
+ section.join "\n"
449
+ end
450
+
451
+ ##
452
+ # The path to sphinx.conf.
453
+
454
+ def self.sphinx_conf
455
+ @sphinx_conf ||= File.join sphinx_dir, 'sphinx.conf'
456
+ end
457
+
458
+ ##
459
+ # The directory where sphinx's files live.
460
+
461
+ def self.sphinx_dir
462
+ @sphinx_dir ||= File.join(RAILS_ROOT,
463
+ Sphincter::Configure.get_conf['sphincter']['path'])
464
+ end
465
+
466
+ ##
467
+ # Writes out a sphinx.conf configuration using +conf+ and +sources+.
468
+
469
+ def self.write_configuration(conf, sources)
470
+ FileUtils.mkdir_p sphinx_dir
471
+
472
+ out = []
473
+
474
+ out << section('indexer', conf['indexer'])
475
+ out << nil
476
+
477
+ out << section('searchd', conf['searchd'])
478
+ out << nil
479
+
480
+ sources.each do |index_name, values|
481
+ source_data = conf['source'].merge values
482
+ out << section("source #{index_name}", source_data)
483
+ out << nil
484
+
485
+ index_path = File.join sphinx_dir, index_name
486
+ index_data = conf['index'].merge 'source' => index_name,
487
+ 'path' => index_path
488
+
489
+ out << section("index #{index_name}", index_data)
490
+ out << nil
491
+ end
492
+
493
+ File.open sphinx_conf, 'w' do |fp|
494
+ fp.write out.join("\n")
495
+ end
496
+ end
497
+
498
+ end
499
+