dm-sphinx-adapter 0.4 → 0.5
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 +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
|