Sphincter 1.0.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 +5 -0
- data/LICENSE.txt +27 -0
- data/Manifest.txt +16 -0
- data/README.txt +132 -0
- data/Rakefile +21 -0
- data/lib/sphincter.rb +102 -0
- data/lib/sphincter/association_searcher.rb +22 -0
- data/lib/sphincter/configure.rb +380 -0
- data/lib/sphincter/search.rb +173 -0
- data/lib/sphincter/search_stub.rb +60 -0
- data/lib/sphincter/tasks.rb +64 -0
- data/test/sphincter_test_case.rb +190 -0
- data/test/test_sphincter_association_searcher.rb +39 -0
- data/test/test_sphincter_configure.rb +318 -0
- data/test/test_sphincter_search.rb +100 -0
- data/test/test_sphincter_search_stub.rb +50 -0
- metadata +100 -0
data/History.txt
ADDED
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,132 @@
|
|
1
|
+
Sphincter
|
2
|
+
|
3
|
+
Eric Hodel <drbrain@segment7.net>
|
4
|
+
|
5
|
+
http://seattlerb.org/Sphincter
|
6
|
+
|
7
|
+
Sphincter was named by David Yeu.
|
8
|
+
|
9
|
+
== DESCRIPTION:
|
10
|
+
|
11
|
+
Sphincter is an ActiveRecord extension for full-text searching with Sphinx.
|
12
|
+
|
13
|
+
Sphincter uses Dmytro Shteflyuk's sphinx Ruby API and automatic
|
14
|
+
configuration to make totally rad ActiveRecord searching. Well, you
|
15
|
+
still have to tell Sphincter what models you want to search. It
|
16
|
+
doesn't read your mind.
|
17
|
+
|
18
|
+
For complete documentation:
|
19
|
+
|
20
|
+
ri Sphincter
|
21
|
+
|
22
|
+
== FEATURES:
|
23
|
+
|
24
|
+
* Automatically configures itself.
|
25
|
+
* Handy set of rake tasks for easy, automatic management.
|
26
|
+
* Automatically adds has_many metadata for searching across the
|
27
|
+
association.
|
28
|
+
* Stub for testing without connecting to searchd, Sphincter::SearchStub.
|
29
|
+
* Easy pagination support.
|
30
|
+
* Filtering by index metadata and ranges, including dates.
|
31
|
+
|
32
|
+
== PROBLEMS:
|
33
|
+
|
34
|
+
* Setting match mode not supported.
|
35
|
+
* Setting sort mode not supported.
|
36
|
+
* Setting per-field weights not supported.
|
37
|
+
* Setting id range not supported.
|
38
|
+
* Setting group-by not supported.
|
39
|
+
|
40
|
+
== QUICK-START:
|
41
|
+
|
42
|
+
Download and install Sphinx from http://www.sphinxsearch.com/downloads.html
|
43
|
+
|
44
|
+
Download Sphinx Ruby API from http://rubyforge.org/frs/?group_id=2604&release_id=11049
|
45
|
+
|
46
|
+
Unpack Sphinx Ruby API into vendor/plugins/.
|
47
|
+
|
48
|
+
Install Sphincter:
|
49
|
+
|
50
|
+
$ gem install Sphincter
|
51
|
+
|
52
|
+
Load Sphincter in config/environment.rb:
|
53
|
+
|
54
|
+
require 'sphincter'
|
55
|
+
|
56
|
+
By default, Sphincter will run searchd on the same port for all
|
57
|
+
environments. See Sphincter::Configure for how to configure different
|
58
|
+
environments to use different ports.
|
59
|
+
|
60
|
+
Add indexes to models:
|
61
|
+
|
62
|
+
class Post < ActiveRecord::Base
|
63
|
+
belongs_to :blog
|
64
|
+
add_index :fields => %w[title body published]
|
65
|
+
end
|
66
|
+
|
67
|
+
Add searching UI:
|
68
|
+
|
69
|
+
class BlogController < ApplicationController
|
70
|
+
def search
|
71
|
+
@blog = Blog.find params[:id]
|
72
|
+
|
73
|
+
@results = @blog.posts.search params[:q]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Start searchd:
|
78
|
+
|
79
|
+
$ rake sphincter:start_searchd
|
80
|
+
|
81
|
+
Then test it out in your browser.
|
82
|
+
|
83
|
+
== TESTING QUICK-START:
|
84
|
+
|
85
|
+
See Sphinx::SearchStub.
|
86
|
+
|
87
|
+
== EXAMPLES:
|
88
|
+
|
89
|
+
See Sphincter::Search#search for full documentation.
|
90
|
+
|
91
|
+
Example ActiveRecord model:
|
92
|
+
|
93
|
+
class Post < ActiveRecord::Base
|
94
|
+
belongs_to :blog
|
95
|
+
|
96
|
+
# published is a boolean and title and body are string or text fields
|
97
|
+
add_index :fields => %w[title body published]
|
98
|
+
end
|
99
|
+
|
100
|
+
Simple search:
|
101
|
+
|
102
|
+
Post.search 'words'
|
103
|
+
|
104
|
+
Only search published posts:
|
105
|
+
|
106
|
+
Post.search 'words', :conditions => { :published => 1 }
|
107
|
+
|
108
|
+
Only search posts created in the last week:
|
109
|
+
|
110
|
+
now = Time.now
|
111
|
+
ago = now - 1.weeks
|
112
|
+
Post.search 'words', :between => { :created_on => [ago, now] }
|
113
|
+
|
114
|
+
Pagination (defaults to ten records/page):
|
115
|
+
|
116
|
+
Post.search 'words', :page => 2
|
117
|
+
|
118
|
+
Pagination with custom page size:
|
119
|
+
|
120
|
+
Post.search 'words', :page => 2, :per_page => 20
|
121
|
+
|
122
|
+
Pagination with custom page size (better):
|
123
|
+
|
124
|
+
Add to config/sphincter.yml:
|
125
|
+
|
126
|
+
sphincter:
|
127
|
+
per_page: 20
|
128
|
+
|
129
|
+
Then search:
|
130
|
+
|
131
|
+
Post.search 'words', :page => 2
|
132
|
+
|
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', 5).first
|
13
|
+
p.description = p.paragraphs_of('README.txt', 6).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,102 @@
|
|
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.0.0'
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
require 'sphincter/configure'
|
100
|
+
require 'sphincter/association_searcher'
|
101
|
+
require 'sphincter/search'
|
102
|
+
|
@@ -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,380 @@
|
|
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
|
+
@env_conf = nil
|
54
|
+
@index_count = nil
|
55
|
+
|
56
|
+
rails_env = defined?(RAILS_ENV) ? RAILS_ENV : 'RAILS_ENV'
|
57
|
+
|
58
|
+
##
|
59
|
+
# Default Sphincter configuration.
|
60
|
+
|
61
|
+
DEFAULT_CONF = {
|
62
|
+
'sphincter' => {
|
63
|
+
'address' => '127.0.0.1',
|
64
|
+
'path' => "sphinx/#{rails_env}",
|
65
|
+
'per_page' => 10,
|
66
|
+
'port' => 3312,
|
67
|
+
},
|
68
|
+
|
69
|
+
'index' => {
|
70
|
+
'charset_type' => 'utf-8',
|
71
|
+
'docinfo' => 'extern',
|
72
|
+
'min_word_len' => 1,
|
73
|
+
'morphology' => 'stem_en',
|
74
|
+
'stopwords' => '',
|
75
|
+
},
|
76
|
+
|
77
|
+
'indexer' => {
|
78
|
+
'mem_limit' => '32M',
|
79
|
+
},
|
80
|
+
|
81
|
+
'mysql' => {
|
82
|
+
'sql_query_pre' => [
|
83
|
+
'SET SESSION group_concat_max_len = 65535',
|
84
|
+
'SET NAMES utf8',
|
85
|
+
],
|
86
|
+
},
|
87
|
+
|
88
|
+
'searchd' => {
|
89
|
+
'log' => "log/sphinx/searchd.#{rails_env}.log",
|
90
|
+
'max_children' => 30,
|
91
|
+
'max_matches' => 1000,
|
92
|
+
'query_log' => "log/sphinx/query.#{rails_env}.log",
|
93
|
+
'read_timeout' => 5,
|
94
|
+
},
|
95
|
+
|
96
|
+
'source' => {
|
97
|
+
'index_html_attrs' => '',
|
98
|
+
'sql_query_post' => '',
|
99
|
+
'sql_range_step' => 20000,
|
100
|
+
'strip_html' => 0,
|
101
|
+
},
|
102
|
+
}
|
103
|
+
|
104
|
+
##
|
105
|
+
# Builds and writes out a sphinx.conf file.
|
106
|
+
|
107
|
+
def self.configure
|
108
|
+
conf = get_conf
|
109
|
+
db_conf = get_db_conf
|
110
|
+
|
111
|
+
db_conf = conf[db_conf['type']].merge db_conf
|
112
|
+
|
113
|
+
sources = get_sources
|
114
|
+
|
115
|
+
sources.each do |name, source_conf|
|
116
|
+
sources[name] = db_conf.merge source_conf
|
117
|
+
end
|
118
|
+
|
119
|
+
write_configuration conf, sources
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Merges Hashes of Hashes +mergee+ and +hash+.
|
124
|
+
|
125
|
+
def self.deep_merge(mergee, hash)
|
126
|
+
mergee = mergee.dup
|
127
|
+
hash.keys.each do |key| mergee[key] ||= hash[key] end
|
128
|
+
mergee.each do |key, value|
|
129
|
+
next unless hash[key]
|
130
|
+
mergee[key] = value.merge hash[key]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Builds the Sphincter configuration.
|
136
|
+
#
|
137
|
+
# Automatically fills in searchd address, port and pid_file from 'sphincter'
|
138
|
+
# section.
|
139
|
+
|
140
|
+
def self.get_conf
|
141
|
+
return @env_conf unless @env_conf.nil?
|
142
|
+
|
143
|
+
base_file = File.expand_path File.join(RAILS_ROOT, 'config', 'sphincter.yml')
|
144
|
+
base_conf = deep_merge DEFAULT_CONF, get_conf_from(base_file)
|
145
|
+
|
146
|
+
env_file = File.expand_path File.join(RAILS_ROOT, 'config', 'environments',
|
147
|
+
"sphincter.#{RAILS_ENV}.yml")
|
148
|
+
env_conf = deep_merge base_conf, get_conf_from(env_file)
|
149
|
+
|
150
|
+
env_conf['searchd']['address'] = env_conf['sphincter']['address']
|
151
|
+
env_conf['searchd']['port'] = env_conf['sphincter']['port']
|
152
|
+
env_conf['searchd']['pid_file'] = File.join(env_conf['sphincter']['path'],
|
153
|
+
'searchd.pid')
|
154
|
+
|
155
|
+
@env_conf = env_conf
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Reads configuration file +file+. Returns {} if the file does not exist.
|
160
|
+
|
161
|
+
def self.get_conf_from(file)
|
162
|
+
if File.exist? file then
|
163
|
+
YAML.load File.read(file)
|
164
|
+
else
|
165
|
+
{}
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Builds a sphinx.conf source configuration for each index.
|
171
|
+
|
172
|
+
def self.get_sources
|
173
|
+
load_models
|
174
|
+
|
175
|
+
indexes = Sphincter::Search.indexes
|
176
|
+
index_count # HACK necessary to set options[:index_id] per-index
|
177
|
+
|
178
|
+
sources = {}
|
179
|
+
|
180
|
+
indexes.each do |klass, model_indexes|
|
181
|
+
model_indexes.each do |options|
|
182
|
+
conn = klass.connection
|
183
|
+
table = klass.table_name
|
184
|
+
pk = conn.quote_column_name klass.primary_key
|
185
|
+
index_id = options[:index_id]
|
186
|
+
|
187
|
+
source_conf = {}
|
188
|
+
source_conf['sql_date_column'] = []
|
189
|
+
source_conf['sql_group_column'] = %w[sphincter_index_id]
|
190
|
+
|
191
|
+
fields = []
|
192
|
+
fields << "(#{table}.#{pk} * #{index_count} + #{index_id}) AS #{pk}"
|
193
|
+
fields << "#{index_id} AS sphincter_index_id"
|
194
|
+
fields << "#{conn.quote table} AS klass"
|
195
|
+
|
196
|
+
options[:fields].each do |field|
|
197
|
+
fields << get_sources_field(source_conf, klass, field)
|
198
|
+
end
|
199
|
+
|
200
|
+
fields = fields.join ', '
|
201
|
+
|
202
|
+
where = []
|
203
|
+
where << "#{table}.#{pk} >= $start AND #{table}.#{pk} <= $end"
|
204
|
+
where << options[:conditions]
|
205
|
+
where = where.compact.join ' AND '
|
206
|
+
|
207
|
+
source_conf['sql_query'] =
|
208
|
+
"SELECT #{fields} FROM #{table} WHERE #{where}"
|
209
|
+
source_conf['sql_query_info'] =
|
210
|
+
"SELECT * FROM #{table} " \
|
211
|
+
"WHERE #{table}.#{pk} = (($id - #{index_id}) / #{index_count})"
|
212
|
+
source_conf['sql_query_range'] =
|
213
|
+
"SELECT MIN(#{pk}), MAX(#{pk}) FROM #{table}"
|
214
|
+
source_conf['strip_html'] = options[:strip_html] ? 1 : 0
|
215
|
+
|
216
|
+
name = options[:name] || table
|
217
|
+
sources[name] = source_conf
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
sources
|
222
|
+
end
|
223
|
+
|
224
|
+
##
|
225
|
+
# Builds a field for a source's sql_query sphinx.conf setting.
|
226
|
+
#
|
227
|
+
# get_sources_field only understands :datetime, :boolean, :integer, :string
|
228
|
+
# and :text column types.
|
229
|
+
|
230
|
+
def self.get_sources_field(source_conf, klass, field)
|
231
|
+
conn = klass.connection
|
232
|
+
table = klass.table_name
|
233
|
+
|
234
|
+
quoted_field = conn.quote_column_name field
|
235
|
+
case klass.columns_hash[field].type
|
236
|
+
when :date, :datetime, :time, :timestamp then
|
237
|
+
source_conf['sql_date_column'] << field
|
238
|
+
"UNIX_TIMESTAMP(#{table}.#{quoted_field}) AS #{quoted_field}"
|
239
|
+
when :boolean, :integer then
|
240
|
+
source_conf['sql_group_column'] << field
|
241
|
+
"#{table}.#{quoted_field} AS #{quoted_field}"
|
242
|
+
when :string, :text then
|
243
|
+
"#{table}.#{quoted_field} AS #{quoted_field}"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
##
|
248
|
+
# Retrieves the database configuration for ActiveRecord::Base and adapts it
|
249
|
+
# for a sphinx.conf file.
|
250
|
+
|
251
|
+
def self.get_db_conf
|
252
|
+
conf = {}
|
253
|
+
ar_conf = ActiveRecord::Base.configurations[::RAILS_ENV]
|
254
|
+
|
255
|
+
conf['type'] = ar_conf['adapter']
|
256
|
+
conf['sql_host'] = ar_conf['host'] if ar_conf.include? 'host'
|
257
|
+
conf['sql_user'] = ar_conf['username'] if ar_conf.include? 'username'
|
258
|
+
conf['sql_pass'] = ar_conf['password'] if ar_conf.include? 'password'
|
259
|
+
conf['sql_db'] = ar_conf['database'] if ar_conf.include? 'database'
|
260
|
+
conf['sql_sock'] = ar_conf['socket'] if ar_conf.include? 'socket'
|
261
|
+
|
262
|
+
conf
|
263
|
+
end
|
264
|
+
|
265
|
+
##
|
266
|
+
# Iterates over the searchable ActiveRecord::Base classes and assigns an
|
267
|
+
# index to each one. Returns the total number of indexes found.
|
268
|
+
|
269
|
+
def self.index_count
|
270
|
+
return @index_count unless @index_count.nil?
|
271
|
+
|
272
|
+
@index_count = 0
|
273
|
+
|
274
|
+
load_models
|
275
|
+
|
276
|
+
Sphincter::Search.indexes.each do |model, model_indexes|
|
277
|
+
model_indexes.each do |options|
|
278
|
+
options[:index_id] = @index_count
|
279
|
+
@index_count += 1
|
280
|
+
end
|
281
|
+
end
|
282
|
+
@index_count
|
283
|
+
end
|
284
|
+
|
285
|
+
##
|
286
|
+
# Loads ActiveRecord::Base models from app/models.
|
287
|
+
|
288
|
+
def self.load_models
|
289
|
+
model_files = Dir[File.join(RAILS_ROOT, 'app', 'models', '*.rb')]
|
290
|
+
model_names = model_files.map { |name| File.basename name, '.rb' }
|
291
|
+
model_names.each { |name| name.camelize.constantize }
|
292
|
+
end
|
293
|
+
|
294
|
+
##
|
295
|
+
# Returns the pid of searchd if searchd is running, otherwise false.
|
296
|
+
|
297
|
+
def self.searchd_running?
|
298
|
+
pid_file = Sphincter::Configure.get_conf['searchd']['pid_file']
|
299
|
+
return false unless File.exist? pid_file
|
300
|
+
|
301
|
+
pid = File.read pid_file
|
302
|
+
return false if pid.empty?
|
303
|
+
|
304
|
+
running = `ps -p #{pid}` =~ /#{pid}.*searchd/
|
305
|
+
running ? pid : false
|
306
|
+
end
|
307
|
+
|
308
|
+
##
|
309
|
+
# Outputs a sphinx.conf configuration section titled +heading+ using the
|
310
|
+
# Hash +data+. Values in +data+ may be a String or Array. For an Array,
|
311
|
+
# the Hash key is printed multiple times.
|
312
|
+
|
313
|
+
def self.section(heading, data)
|
314
|
+
section = []
|
315
|
+
section << heading
|
316
|
+
section << '{'
|
317
|
+
data.sort_by { |k,| k }.each do |key, value|
|
318
|
+
case value
|
319
|
+
when Array then
|
320
|
+
next if value.empty?
|
321
|
+
value.each do |v|
|
322
|
+
section << " #{key} = #{v}"
|
323
|
+
end
|
324
|
+
else
|
325
|
+
section << " #{key} = #{value}"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
section << '}'
|
329
|
+
section.join "\n"
|
330
|
+
end
|
331
|
+
|
332
|
+
##
|
333
|
+
# The path to sphinx.conf.
|
334
|
+
|
335
|
+
def self.sphinx_conf
|
336
|
+
@sphinx_conf ||= File.join sphinx_dir, 'sphinx.conf'
|
337
|
+
end
|
338
|
+
|
339
|
+
##
|
340
|
+
# The directory where sphinx's files live.
|
341
|
+
|
342
|
+
def self.sphinx_dir
|
343
|
+
@sphinx_dir ||= File.join(RAILS_ROOT,
|
344
|
+
Sphincter::Configure.get_conf['sphincter']['path'])
|
345
|
+
end
|
346
|
+
|
347
|
+
##
|
348
|
+
# Writes out a sphinx.conf configuration using +conf+ and +sources+.
|
349
|
+
|
350
|
+
def self.write_configuration(conf, sources)
|
351
|
+
FileUtils.mkdir_p sphinx_dir
|
352
|
+
|
353
|
+
out = []
|
354
|
+
|
355
|
+
out << section('indexer', conf['indexer'])
|
356
|
+
out << nil
|
357
|
+
|
358
|
+
out << section('searchd', conf['searchd'])
|
359
|
+
out << nil
|
360
|
+
|
361
|
+
sources.each do |index_name, values|
|
362
|
+
source_data = conf['source'].merge values
|
363
|
+
out << section("source #{index_name}", source_data)
|
364
|
+
out << nil
|
365
|
+
|
366
|
+
index_path = File.join sphinx_dir, index_name
|
367
|
+
index_data = conf['index'].merge 'source' => index_name,
|
368
|
+
'path' => index_path
|
369
|
+
|
370
|
+
out << section("index #{index_name}", index_data)
|
371
|
+
out << nil
|
372
|
+
end
|
373
|
+
|
374
|
+
File.open sphinx_conf, 'w' do |fp|
|
375
|
+
fp.write out.join("\n")
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
end
|
380
|
+
|