dm-sphinx-adapter 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +12 -0
- data/Manifest.txt +24 -13
- data/README.txt +44 -28
- data/Rakefile +1 -1
- data/dm-sphinx-adapter.gemspec +7 -7
- data/lib/dm-sphinx-adapter.rb +8 -6
- data/lib/dm-sphinx-adapter/adapter.rb +218 -0
- data/lib/dm-sphinx-adapter/attribute.rb +38 -0
- data/lib/dm-sphinx-adapter/client.rb +109 -0
- data/lib/dm-sphinx-adapter/config.rb +122 -0
- data/lib/dm-sphinx-adapter/config_parser.rb +71 -0
- data/lib/dm-sphinx-adapter/index.rb +38 -0
- data/lib/dm-sphinx-adapter/query.rb +67 -0
- data/lib/dm-sphinx-adapter/resource.rb +103 -0
- data/test/{fixtures/item.sql → files/dm_sphinx_adapter_test.sql} +3 -3
- data/test/{fixtures/item_resource_explicit.rb → files/resource_explicit.rb} +5 -6
- data/test/{fixtures/item_resource_only.rb → files/resource_resource.rb} +4 -5
- data/test/{fixtures/item.rb → files/resource_searchable.rb} +8 -5
- data/test/files/resource_storage_name.rb +11 -0
- data/test/files/resource_vanilla.rb +7 -0
- data/test/{data → files}/sphinx.conf +11 -6
- data/test/test_adapter.rb +38 -0
- data/test/test_adapter_explicit.rb +48 -0
- data/test/test_adapter_resource.rb +25 -0
- data/test/test_adapter_searchable.rb +23 -0
- data/test/test_adapter_vanilla.rb +46 -0
- data/test/test_client.rb +9 -21
- data/test/test_config.rb +58 -24
- data/test/test_config_parser.rb +29 -0
- data/test/test_query.rb +47 -0
- data/test/test_type_attribute.rb +8 -0
- data/test/test_type_index.rb +8 -0
- metadata +38 -19
- data/lib/dm-sphinx-adapter/sphinx_adapter.rb +0 -220
- data/lib/dm-sphinx-adapter/sphinx_attribute.rb +0 -22
- data/lib/dm-sphinx-adapter/sphinx_client.rb +0 -81
- data/lib/dm-sphinx-adapter/sphinx_config.rb +0 -160
- data/lib/dm-sphinx-adapter/sphinx_index.rb +0 -21
- data/lib/dm-sphinx-adapter/sphinx_resource.rb +0 -88
- data/test/helper.rb +0 -7
- data/test/test_search.rb +0 -52
@@ -1,22 +0,0 @@
|
|
1
|
-
module DataMapper
|
2
|
-
class SphinxAttribute < Property
|
3
|
-
|
4
|
-
# DataMapper types supported as Sphinx attributes.
|
5
|
-
TYPES = [
|
6
|
-
TrueClass, # sql_attr_bool
|
7
|
-
String, # sql_attr_str2ordinal
|
8
|
-
# DataMapper::Types::Text,
|
9
|
-
Float, # sql_attr_float
|
10
|
-
Integer, # sql_attr_uint
|
11
|
-
# BigDecimal, # sql_attr_float?
|
12
|
-
DateTime, # sql_attr_timestamp
|
13
|
-
Date, # sql_attr_timestamp
|
14
|
-
Time, # sql_attr_timestamp
|
15
|
-
# Object,
|
16
|
-
# Class,
|
17
|
-
# DataMapper::Types::Discriminator,
|
18
|
-
DataMapper::Types::Serial # sql_attr_uint
|
19
|
-
]
|
20
|
-
|
21
|
-
end # SphinxAttribute
|
22
|
-
end # DataMapper
|
@@ -1,81 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
|
3
|
-
gem 'riddle', '~> 0.9'
|
4
|
-
require 'riddle'
|
5
|
-
|
6
|
-
module DataMapper
|
7
|
-
class SphinxClient
|
8
|
-
def initialize(uri_or_options)
|
9
|
-
# TODO: Documentation.
|
10
|
-
@config = SphinxConfig.new(uri_or_options)
|
11
|
-
end
|
12
|
-
|
13
|
-
##
|
14
|
-
# Search one or more indexes.
|
15
|
-
#
|
16
|
-
# @param [String] query The sphinx query string.
|
17
|
-
# @param [Array, String] indexes A string or array of indexes to search. Default is '*' (all).
|
18
|
-
# @param [Hash] options Any options you'd like to pass through to Riddle::Client.
|
19
|
-
# @see Riddle::Client
|
20
|
-
def search(query, indexes = '*', options = {})
|
21
|
-
indexes = indexes.join(' ') if indexes.kind_of?(Array)
|
22
|
-
|
23
|
-
client = Riddle::Client.new(@config.address, @config.port)
|
24
|
-
options.each{|k, v| client.method("#{k}=".to_sym).call(v) if client.respond_to?("#{k}=".to_sym)}
|
25
|
-
client.query(query, indexes.to_s)
|
26
|
-
end
|
27
|
-
|
28
|
-
##
|
29
|
-
# Index one or more indexes.
|
30
|
-
#
|
31
|
-
# @param [Array, String] indexes Defaults to --all if indexes is nil or '*'.
|
32
|
-
def index(indexes = nil, options = {})
|
33
|
-
indexes = indexes.join(' ') if indexes.kind_of?(Array)
|
34
|
-
|
35
|
-
command = @config.indexer_bin
|
36
|
-
command << " --rotate" if running?
|
37
|
-
command << ((indexes.nil? || indexes == '*') ? ' --all' : " #{indexes.to_s}")
|
38
|
-
warn "Sphinx: Indexer #{$1}" if `#{command}` =~ /(?:error|fatal|warning):?\s*([^\n]+)/i
|
39
|
-
end
|
40
|
-
|
41
|
-
protected
|
42
|
-
|
43
|
-
##
|
44
|
-
# Is the client running.
|
45
|
-
#
|
46
|
-
# Tests the address and port set in the configuration file.
|
47
|
-
def running?
|
48
|
-
!!TCPSocket.new(@config.address, @config.port) rescue nil
|
49
|
-
end
|
50
|
-
end # SphinxClient
|
51
|
-
|
52
|
-
##
|
53
|
-
# Managed searchd if you don't already have god/monit doing the job for you.
|
54
|
-
#
|
55
|
-
# Requires you have daemon_controller installed.
|
56
|
-
# @see http://github.com/FooBarWidget/daemon_controller/tree/master
|
57
|
-
class SphinxManagedClient < SphinxClient
|
58
|
-
def initialize(url_or_options)
|
59
|
-
super
|
60
|
-
|
61
|
-
# Fire up searchd.
|
62
|
-
require 'daemon_controller'
|
63
|
-
@client = DaemonController.new(
|
64
|
-
:identifier => 'Sphinx searchd',
|
65
|
-
:start_command => @config.searchd_bin,
|
66
|
-
:stop_command => "#{@config.searchd_bin} --stop",
|
67
|
-
:ping_command => method(:running?),
|
68
|
-
:pid_file => @config.pid_file,
|
69
|
-
:log_file => @config.log
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
|
-
def search(*args)
|
74
|
-
@client.connect{super}
|
75
|
-
end
|
76
|
-
|
77
|
-
def stop
|
78
|
-
@client.stop
|
79
|
-
end
|
80
|
-
end # SphinxManagedClient
|
81
|
-
end # DataMapper
|
@@ -1,160 +0,0 @@
|
|
1
|
-
require 'strscan'
|
2
|
-
require 'pathname'
|
3
|
-
|
4
|
-
# TODO: Error classes.
|
5
|
-
# TODO: Just warn if a config file can't be found.
|
6
|
-
|
7
|
-
module DataMapper
|
8
|
-
class SphinxConfig
|
9
|
-
|
10
|
-
##
|
11
|
-
# Read a sphinx configuration file.
|
12
|
-
#
|
13
|
-
# This class just gives you access to handy searchd {} configuration options. It does not validate your
|
14
|
-
# configuration file beyond basic syntax checking.
|
15
|
-
def initialize(uri_or_options = {})
|
16
|
-
@config = []
|
17
|
-
@searchd = {
|
18
|
-
'address' => '0.0.0.0',
|
19
|
-
'log' => 'searchd.log',
|
20
|
-
'max_children' => 0,
|
21
|
-
'max_matches' => 1000,
|
22
|
-
'pid_file' => nil,
|
23
|
-
'port' => 3312,
|
24
|
-
'preopen_indexes' => 0,
|
25
|
-
'query_log' => '',
|
26
|
-
'read_timeout' => 5,
|
27
|
-
'seamless_rotate' => 1,
|
28
|
-
'unlink_old' => 1
|
29
|
-
}
|
30
|
-
|
31
|
-
path = case uri_or_options
|
32
|
-
when Addressable::URI, DataObjects::URI then uri_or_options.path
|
33
|
-
when Hash then uri_or_options[:config] || uri_or_options[:database]
|
34
|
-
when Pathname then uri_or_options
|
35
|
-
when String then DataObjects::URI.parse(uri_or_options).path
|
36
|
-
end
|
37
|
-
parse('' + path.to_s) # Force stringy since Pathname#to_s is broken IMO.
|
38
|
-
end
|
39
|
-
|
40
|
-
##
|
41
|
-
# Configuration file full path name.
|
42
|
-
#
|
43
|
-
# @return [String]
|
44
|
-
def config
|
45
|
-
@config
|
46
|
-
end
|
47
|
-
|
48
|
-
##
|
49
|
-
# Indexer binary full path name and config argument.
|
50
|
-
def indexer_bin(use_config = true)
|
51
|
-
path = 'indexer' # TODO: Real.
|
52
|
-
path << " --config #{config}" if config
|
53
|
-
path
|
54
|
-
end
|
55
|
-
|
56
|
-
##
|
57
|
-
# Searchd binardy full path name and config argument.
|
58
|
-
def searchd_bin(use_config = true)
|
59
|
-
path = 'searchd' # TODO: Real.
|
60
|
-
path << " --config #{config}" if config
|
61
|
-
path
|
62
|
-
end
|
63
|
-
|
64
|
-
##
|
65
|
-
# Searchd address.
|
66
|
-
def address
|
67
|
-
searchd['address']
|
68
|
-
end
|
69
|
-
|
70
|
-
##
|
71
|
-
# Searchd port.
|
72
|
-
def port
|
73
|
-
searchd['port']
|
74
|
-
end
|
75
|
-
|
76
|
-
##
|
77
|
-
# Searchd pid_file.
|
78
|
-
def pid_file
|
79
|
-
searchd['pid_file'] or raise "Mandatory pid_file option missing from searchd configuration."
|
80
|
-
end
|
81
|
-
|
82
|
-
##
|
83
|
-
# Searchd log file.
|
84
|
-
def log
|
85
|
-
searchd['log']
|
86
|
-
end
|
87
|
-
|
88
|
-
##
|
89
|
-
# Searchd configuration options.
|
90
|
-
#
|
91
|
-
# @see http://www.sphinxsearch.com/doc.html#confgroup-searchd
|
92
|
-
def searchd
|
93
|
-
@searchd
|
94
|
-
end
|
95
|
-
|
96
|
-
protected
|
97
|
-
|
98
|
-
##
|
99
|
-
# Parse a sphinx config file.
|
100
|
-
#
|
101
|
-
# @param [String] path Searches path, ./path, /path, /usr/local/etc/sphinx.conf, ./sphinx.conf in that order.
|
102
|
-
def parse(path = '')
|
103
|
-
# TODO: Three discrete things going on here, should be three subs.
|
104
|
-
paths = [
|
105
|
-
path,
|
106
|
-
path.gsub(%r{^/}, './'),
|
107
|
-
path.gsub(%r{^\./}, '/'),
|
108
|
-
'/usr/local/etc/sphinx.conf', # TODO: Does this one depend on where searchd/indexer is installed?
|
109
|
-
'./sphinx.conf'
|
110
|
-
]
|
111
|
-
paths.find do |path|
|
112
|
-
@config = Pathname.new(path).expand_path
|
113
|
-
@config.readable? && `#{indexer_bin}` !~ /fatal|error/i
|
114
|
-
end or raise IOError, %{No readable config file (looked in #{paths.join(', ')})}
|
115
|
-
|
116
|
-
source = config.read
|
117
|
-
source.gsub!(/\r\n|\r|\n/, "\n") # Everything in \n
|
118
|
-
source.gsub!(/\s*\\\n\s*/, ' ') # Remove unixy line wraps.
|
119
|
-
@in = StringScanner.new(source)
|
120
|
-
blocks(blocks = [])
|
121
|
-
@in = nil
|
122
|
-
|
123
|
-
searchd = blocks.find{|c| c['type'] =~ /searchd/i} || {}
|
124
|
-
@searchd.update(searchd)
|
125
|
-
end
|
126
|
-
|
127
|
-
private
|
128
|
-
|
129
|
-
def blocks(out = []) #:nodoc:
|
130
|
-
if @in.scan(/\#[^\n]*\n/) || @in.scan(/\s+/)
|
131
|
-
blocks(out)
|
132
|
-
elsif @in.scan(/indexer|searchd|source|index/i)
|
133
|
-
out << group = {'type' => @in.matched}
|
134
|
-
if @in.matched =~ /^(?:index|source)$/i
|
135
|
-
@in.scan(/\s* ([\w_\-]+) (?:\s*:\s*([\w_\-]+))? \s*/x) or raise "Expected #{group[:type]} name."
|
136
|
-
group['name'] = @in[1]
|
137
|
-
group['ancestor'] = @in[2]
|
138
|
-
end
|
139
|
-
@in.scan(/\s*\{/) or raise %q{Expected '\{'.}
|
140
|
-
pairs(kv = {})
|
141
|
-
group.merge!(kv)
|
142
|
-
@in.scan(/\s*\}/) or raise %q{Expected '\}'.}
|
143
|
-
blocks(out)
|
144
|
-
else
|
145
|
-
raise "Unknown near: #{@in.peek(30)}" unless @in.eos?
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
def pairs(out = {}) #:nodoc:
|
150
|
-
if @in.scan(/\#[^\n]*\n/) || @in.scan(/\s+/)
|
151
|
-
pairs(out)
|
152
|
-
elsif @in.scan(/[\w_-]+/)
|
153
|
-
key = @in.matched
|
154
|
-
@in.scan(/\s*=/) or raise %q{Expected '='.}
|
155
|
-
out[key] = @in.scan(/[^\n]*\n/).strip
|
156
|
-
pairs(out)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end # SphinxConfig
|
160
|
-
end # DataMapper
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module DataMapper
|
2
|
-
class SphinxIndex
|
3
|
-
include Assertions
|
4
|
-
|
5
|
-
attr_reader :model, :name, :options
|
6
|
-
|
7
|
-
def initialize(model, name, options = {})
|
8
|
-
assert_kind_of 'model', model, Model
|
9
|
-
assert_kind_of 'name', name, Symbol, String
|
10
|
-
assert_kind_of 'options', options, Hash
|
11
|
-
|
12
|
-
@model = model
|
13
|
-
@name = name.to_sym
|
14
|
-
@delta = options.fetch(:delta, nil)
|
15
|
-
end
|
16
|
-
|
17
|
-
def delta?
|
18
|
-
!!@delta
|
19
|
-
end
|
20
|
-
end # SphinxIndex
|
21
|
-
end # DataMapper
|
@@ -1,88 +0,0 @@
|
|
1
|
-
module DataMapper
|
2
|
-
##
|
3
|
-
# Declare Sphinx indexes in your resource.
|
4
|
-
#
|
5
|
-
# model Items
|
6
|
-
# include Sphinx::Resource
|
7
|
-
#
|
8
|
-
# # .. normal properties and such for :default
|
9
|
-
#
|
10
|
-
# repository(:search) do
|
11
|
-
# # Query some_index, some_index_delta in that order.
|
12
|
-
# index :some_index
|
13
|
-
# index :some_index_delta, :delta => true
|
14
|
-
#
|
15
|
-
# # Sortable by some attributes.
|
16
|
-
# attribute :updated_at, DateTime # sql_attr_timestamp
|
17
|
-
# attribute :age, Integer # sql_attr_uint
|
18
|
-
# attribute :deleted, Boolean # sql_attr_bool
|
19
|
-
# end
|
20
|
-
# end
|
21
|
-
module SphinxResource
|
22
|
-
def self.included(model) #:nodoc:
|
23
|
-
model.class_eval do
|
24
|
-
include DataMapper::Resource
|
25
|
-
extend ClassMethods
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
module ClassMethods
|
30
|
-
def self.extended(model) #:nodoc:
|
31
|
-
model.instance_variable_set(:@sphinx_indexes, {})
|
32
|
-
model.instance_variable_set(:@sphinx_attributes, {})
|
33
|
-
end
|
34
|
-
|
35
|
-
##
|
36
|
-
# Defines a sphinx index on the resource.
|
37
|
-
#
|
38
|
-
# Indexes are naturally ordered, with delta indexes at the end of the list so that duplicate document IDs in
|
39
|
-
# delta indexes override your main indexes.
|
40
|
-
#
|
41
|
-
# @param [Symbol] name the name of a sphinx index to search for this resource
|
42
|
-
# @param [Hash(Symbol => String)] options a hash of available options
|
43
|
-
# @see DataMapper::SphinxIndex
|
44
|
-
def index(name, options = {})
|
45
|
-
index = SphinxIndex.new(self, name, options)
|
46
|
-
indexes = sphinx_indexes(repository_name)
|
47
|
-
indexes << index
|
48
|
-
|
49
|
-
# TODO: I'm such a Ruby nub. In the meantime I've gone back to my Perl roots.
|
50
|
-
# This is a Schwartzian transform to sort delta indexes to the bottom and natural sort by name.
|
51
|
-
mapped = indexes.map{|i| [(i.delta? ? 1 : 0), i.name, i]}
|
52
|
-
sorted = mapped.sort{|a, b| a[0] <=> b[0] || a[1] <=> b[1]}
|
53
|
-
indexes.replace(sorted.map{|i| i[2]})
|
54
|
-
|
55
|
-
index
|
56
|
-
end
|
57
|
-
|
58
|
-
##
|
59
|
-
# List of declared sphinx indexes for this model.
|
60
|
-
def sphinx_indexes(repository_name = default_repository_name)
|
61
|
-
@sphinx_indexes[repository_name] ||= []
|
62
|
-
end
|
63
|
-
|
64
|
-
##
|
65
|
-
# Defines a sphinx attribute on the resource.
|
66
|
-
#
|
67
|
-
# @param [Symbol] name the name of a sphinx attribute to order/restrict by for this resource
|
68
|
-
# @param [Class] type the type to define this attribute as
|
69
|
-
# @param [Hash(Symbol => String)] options a hash of available options
|
70
|
-
# @see DataMapper::SphinxAttribute
|
71
|
-
def attribute(name, type, options = {})
|
72
|
-
# Attributes are just properties without a getter/setter in the model.
|
73
|
-
# This keeps DataMapper::Query happy when building queries.
|
74
|
-
attribute = SphinxAttribute.new(self, name, type, options)
|
75
|
-
properties(repository_name)[attribute.name] = attribute
|
76
|
-
attribute
|
77
|
-
end
|
78
|
-
|
79
|
-
##
|
80
|
-
# List of declared sphinx attributes for this model.
|
81
|
-
def sphinx_attributes(repository_name = default_repository_name)
|
82
|
-
properties(repository_name).grep{|p| p.kind_of? SphinxAttribute}
|
83
|
-
end
|
84
|
-
|
85
|
-
end # ClassMethods
|
86
|
-
end # SphinxResource
|
87
|
-
end # DataMapper
|
88
|
-
|
data/test/helper.rb
DELETED
data/test/test_search.rb
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
|
4
|
-
class TestSearch < Test::Unit::TestCase
|
5
|
-
def setup
|
6
|
-
# TODO: A little too brutal for me.
|
7
|
-
Dir.chdir(File.join(File.dirname(__FILE__), 'fixtures')) do
|
8
|
-
system 'mysql -u root dm_sphinx_adapter_test < item.sql' \
|
9
|
-
or raise %q{Tests require the dm_sphinx_adapter_test.items table.}
|
10
|
-
end
|
11
|
-
|
12
|
-
@config = Pathname.new(__FILE__).dirname.expand_path / 'data' / 'sphinx.conf'
|
13
|
-
DataMapper.setup(:default, 'mysql://localhost/dm_sphinx_adapter_test')
|
14
|
-
DataMapper.setup(:search,
|
15
|
-
:adapter => 'sphinx',
|
16
|
-
:config => @config,
|
17
|
-
:managed => true
|
18
|
-
)
|
19
|
-
end
|
20
|
-
|
21
|
-
def teardown
|
22
|
-
DataMapper.repository(:search).adapter.client.stop
|
23
|
-
# You can also build a new client with the same config and call stop on that.
|
24
|
-
# client = DataMapper::SphinxManagedClient.new(@config)
|
25
|
-
# client.stop
|
26
|
-
end
|
27
|
-
|
28
|
-
def test_search
|
29
|
-
assert_nothing_raised{ Item.search }
|
30
|
-
assert_nothing_raised{ Item.search(:name => 'foo') }
|
31
|
-
assert !Item.search(:name => 'foo').empty?
|
32
|
-
end
|
33
|
-
|
34
|
-
def test_search_resource_only
|
35
|
-
assert_nothing_raised{ ItemResourceOnly.search }
|
36
|
-
assert_nothing_raised{ ItemResourceOnly.search(:name => 'foo') }
|
37
|
-
assert !ItemResourceOnly.search(:name => 'foo').empty?
|
38
|
-
end
|
39
|
-
|
40
|
-
def test_search_resource_explicit
|
41
|
-
assert_nothing_raised{ ItemResourceExplicit.search }
|
42
|
-
assert_nothing_raised{ ItemResourceExplicit.search(:name => 'foo') }
|
43
|
-
assert !ItemResourceExplicit.search(:name => 'foo').empty?
|
44
|
-
end
|
45
|
-
|
46
|
-
def test_search_attributes
|
47
|
-
# Attributes that exist only in :search and the sphinx.
|
48
|
-
assert_nothing_raised do
|
49
|
-
ItemResourceExplicit.search(:updated => (Time.now - 10 .. Time.now + 10))
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end # TestSearch
|