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
@@ -0,0 +1,38 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
module Sphinx
|
4
|
+
|
5
|
+
# Sphinx attribute definition.
|
6
|
+
#
|
7
|
+
# Supports only a subset of DataMapper::Property types that can be used as Sphinx attributes.
|
8
|
+
#
|
9
|
+
# TrueClass:: sql_attr_bool
|
10
|
+
# String:: sql_attr_str2ordinal
|
11
|
+
# Float:: sql_attr_float
|
12
|
+
# Integer:: sql_attr_uint
|
13
|
+
# DateTime:: sql_attr_timestamp
|
14
|
+
# Date:: sql_attr_timestamp
|
15
|
+
# DataMapper::Types::Serial:: sql_attr_uint
|
16
|
+
class Attribute < Property
|
17
|
+
|
18
|
+
# DataMapper types supported as Sphinx attributes.
|
19
|
+
TYPES = [
|
20
|
+
TrueClass, # sql_attr_bool
|
21
|
+
String, # sql_attr_str2ordinal
|
22
|
+
# DataMapper::Types::Text,
|
23
|
+
Float, # sql_attr_float
|
24
|
+
Integer, # sql_attr_uint
|
25
|
+
# BigDecimal, # sql_attr_float?
|
26
|
+
DateTime, # sql_attr_timestamp
|
27
|
+
Date, # sql_attr_timestamp
|
28
|
+
Time, # sql_attr_timestamp
|
29
|
+
# Object,
|
30
|
+
# Class,
|
31
|
+
# DataMapper::Types::Discriminator,
|
32
|
+
DataMapper::Types::Serial # sql_attr_uint
|
33
|
+
]
|
34
|
+
|
35
|
+
end # Attribute
|
36
|
+
end # Sphinx
|
37
|
+
end # Adapters
|
38
|
+
end # DataMapper
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'riddle', '~> 0.9'
|
4
|
+
require 'riddle'
|
5
|
+
|
6
|
+
module DataMapper
|
7
|
+
module Adapters
|
8
|
+
module Sphinx
|
9
|
+
# Client wrapper for Riddle::Client.
|
10
|
+
#
|
11
|
+
# * Simple interface to +searchd+ *and* +indexer+.
|
12
|
+
# * Can read +searchd+ configuration options from your sphinx configuration file.
|
13
|
+
# * Managed +searchd+ using +daemon_controller+ for on demand daemon control in development.
|
14
|
+
class Client
|
15
|
+
|
16
|
+
# ==== Parameters
|
17
|
+
# uri_or_options<URI, DataObject::URI, Addressable::URI, String, Hash, Pathname>::
|
18
|
+
# DataMapper uri or options hash.
|
19
|
+
def initialize(uri_or_options = {})
|
20
|
+
@config = Sphinx::Config.new(uri_or_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Search one or more indexes.
|
24
|
+
#
|
25
|
+
# ==== See
|
26
|
+
# * Riddle::Client
|
27
|
+
#
|
28
|
+
# ==== Parameters
|
29
|
+
# query<String>:: A sphinx query string.
|
30
|
+
# indexes<Array, String>:: Indexes to search. Default is '*' all.
|
31
|
+
# options<Hash>:: Any attributes supported by the Riddle::Client.
|
32
|
+
#
|
33
|
+
# ==== Returns
|
34
|
+
# Hash:: Riddle::Client#query response struct.
|
35
|
+
def search(query, indexes = '*', options = {})
|
36
|
+
indexes = indexes.join(' ') if indexes.kind_of?(Array)
|
37
|
+
|
38
|
+
client = Riddle::Client.new(@config.address, @config.port)
|
39
|
+
options.each{|k, v| client.method("#{k}=".to_sym).call(v) if client.respond_to?("#{k}=".to_sym)}
|
40
|
+
client.query(query, indexes.to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Index one or more indexes.
|
44
|
+
#
|
45
|
+
# ==== Parameters
|
46
|
+
# indexes<Array, String>:: Indexes to index (and rotate). Defaults to --all if indexes is +nil+ or '*'.
|
47
|
+
def index(indexes = nil)
|
48
|
+
indexes = indexes.join(' ') if indexes.kind_of?(Array)
|
49
|
+
|
50
|
+
command = @config.indexer_bin
|
51
|
+
command << " --rotate" if running?
|
52
|
+
command << ((indexes.nil? || indexes == '*') ? ' --all' : " #{indexes.to_s}")
|
53
|
+
warn "Sphinx: Indexer #{$1}" if `#{command}` =~ /(?:error|fatal|warning):?\s*([^\n]+)/i
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
# Is a +searchd+ daemon running on the configured address and port.
|
58
|
+
#
|
59
|
+
# ==== Notes
|
60
|
+
# This is a simple TCPSocket test. It may not be your +searchd+ deamon or even a +searchd+ daemon at all if
|
61
|
+
# your configuration is wrong or another +searchd+ daemon is listen on that port already.
|
62
|
+
#
|
63
|
+
# ==== Returns
|
64
|
+
# Boolean
|
65
|
+
def running?
|
66
|
+
!!TCPSocket.new(@config.address, @config.port) rescue nil
|
67
|
+
end
|
68
|
+
end # Client
|
69
|
+
|
70
|
+
# Managed searchd if you don't already have god/monit doing the job for you.
|
71
|
+
#
|
72
|
+
# Requires you have +daemon_controller+ installed.
|
73
|
+
#
|
74
|
+
# ==== See
|
75
|
+
# * http://github.com/FooBarWidget/daemon_controller/tree/master
|
76
|
+
class ManagedClient < Client
|
77
|
+
|
78
|
+
# ==== See
|
79
|
+
# * DataMapper::Adapters::Sphinx::Client#new
|
80
|
+
def initialize(url_or_options = {})
|
81
|
+
super
|
82
|
+
|
83
|
+
require 'daemon_controller'
|
84
|
+
@client = DaemonController.new(
|
85
|
+
:identifier => 'Sphinx searchd',
|
86
|
+
:start_command => @config.searchd_bin,
|
87
|
+
:stop_command => "#{@config.searchd_bin} --stop",
|
88
|
+
:ping_command => method(:running?),
|
89
|
+
:pid_file => @config.pid_file,
|
90
|
+
:log_file => @config.log
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Start the +searchd+ daemon if it isn't already running then search.
|
95
|
+
#
|
96
|
+
# ==== See
|
97
|
+
# * DataMapper::Adapters::Sphinx::Client#search
|
98
|
+
def search(*args)
|
99
|
+
@client.connect{super}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Stop the +searchd+ daemon if it's running.
|
103
|
+
def stop
|
104
|
+
@client.stop if @client.running?
|
105
|
+
end
|
106
|
+
end # ManagedClient
|
107
|
+
end # Sphinx
|
108
|
+
end # Adapters
|
109
|
+
end # DataMapper
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'extlib', '~> 0.9.7'
|
4
|
+
require 'extlib'
|
5
|
+
|
6
|
+
module DataMapper
|
7
|
+
module Adapters
|
8
|
+
module Sphinx
|
9
|
+
class Config
|
10
|
+
include Extlib::Assertions
|
11
|
+
|
12
|
+
# Configuration option.
|
13
|
+
attr_reader :config, :address, :log, :port
|
14
|
+
|
15
|
+
# Sphinx configuration options.
|
16
|
+
#
|
17
|
+
# This class just gives you access to handy searchd {} configuration options. If a sphinx configuration file
|
18
|
+
# is passed and can be parsed +searchd+ options will be set straight from the file.
|
19
|
+
#
|
20
|
+
# ==== Notes
|
21
|
+
# Option precedence is:
|
22
|
+
# * The options hash.
|
23
|
+
# * The configuration file.
|
24
|
+
# * Sphinx defaults.
|
25
|
+
#
|
26
|
+
# ==== See
|
27
|
+
# http://www.sphinxsearch.com/doc.html#confgroup-searchd
|
28
|
+
#
|
29
|
+
# ==== Parameters
|
30
|
+
# uri_or_options<URI, DataObject::URI, Addressable::URI, String, Hash, Pathname>::
|
31
|
+
# DataMapper uri or options hash.
|
32
|
+
def initialize(uri_or_options = {})
|
33
|
+
assert_kind_of 'uri_or_options', uri_or_options, Addressable::URI, DataObjects::URI, Hash, String, Pathname
|
34
|
+
|
35
|
+
options = normalize_options(uri_or_options)
|
36
|
+
config = parse_config("#{options[:path]}") # Pathname#to_s is broken?
|
37
|
+
@address = options[:host] || config['address'] || '0.0.0.0'
|
38
|
+
@port = options[:port] || config['port'] || 3312
|
39
|
+
@log = options[:log] || config['log'] || 'searchd.log'
|
40
|
+
@pid_file = options[:pid_file] || config['pid_file']
|
41
|
+
end
|
42
|
+
|
43
|
+
# Indexer binary full path name and config argument.
|
44
|
+
#
|
45
|
+
# ==== Parameters
|
46
|
+
# use_config<Boolean>:: Return <tt>--config path/to/config.conf</tt> as part of string. Default +true+.
|
47
|
+
#
|
48
|
+
# ==== Returns
|
49
|
+
# String
|
50
|
+
def indexer_bin(use_config = true)
|
51
|
+
path = 'indexer' # TODO: Real.
|
52
|
+
path << " --config #{config}" if config
|
53
|
+
path
|
54
|
+
end
|
55
|
+
|
56
|
+
# Searchd binary full path name and config argument.
|
57
|
+
#
|
58
|
+
# ==== Parameters
|
59
|
+
# use_config<Boolean>:: Return <tt>--config path/to/config.conf</tt> as part of string. Default +true+.
|
60
|
+
#
|
61
|
+
# ==== Returns
|
62
|
+
# String
|
63
|
+
def searchd_bin(use_config = true)
|
64
|
+
path = 'searchd' # TODO: Real.
|
65
|
+
path << " --config #{config}" if config
|
66
|
+
path
|
67
|
+
end
|
68
|
+
|
69
|
+
# Full path to pid_file.
|
70
|
+
#
|
71
|
+
# ==== Raises
|
72
|
+
# RuntimeError:: If a pid file was not read or set. pid_file is a mandatory searchd option in a sphinx
|
73
|
+
# configuration file.
|
74
|
+
def pid_file
|
75
|
+
@pid_file or raise "Mandatory pid_file option missing from searchd configuration."
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
# Coerce +uri_or_options+ into a +Hash+ of options.
|
80
|
+
#
|
81
|
+
# ==== Parameters
|
82
|
+
# uri_or_options<URI, DataObject::URI, Addressable::URI, String, Hash, Pathname>::
|
83
|
+
# DataMapper uri or options hash.
|
84
|
+
#
|
85
|
+
# ==== Returns
|
86
|
+
# Hash
|
87
|
+
def normalize_options(uri_or_options)
|
88
|
+
case uri_or_options
|
89
|
+
when String, Addressable::URI then DataObjects::URI.parse(uri_or_options).attributes
|
90
|
+
when DataObjects::URI then uri_or_options.attributes
|
91
|
+
when Pathname then {:path => uri_or_options}
|
92
|
+
else
|
93
|
+
uri_or_options[:path] ||= uri_or_options.delete(:config) || uri_or_options.delete(:database)
|
94
|
+
uri_or_options
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Reads your sphinx configuration file if given.
|
99
|
+
#
|
100
|
+
# Also searches default sphinx configuration locations for a config file to parse.
|
101
|
+
#
|
102
|
+
# ==== See
|
103
|
+
# * DataMapper::Adapters::Sphinx::ConfigParser
|
104
|
+
#
|
105
|
+
# ==== Parameters
|
106
|
+
# path<String>:: Path to your configuration file.
|
107
|
+
#
|
108
|
+
# ==== Returns
|
109
|
+
# Hash
|
110
|
+
def parse_config(path)
|
111
|
+
paths = []
|
112
|
+
paths.push(path, path.gsub(%r{^/}, './'), path.gsub(%r{^\./}, '/')) unless path.blank?
|
113
|
+
paths.push('/usr/local/etc/sphinx.conf', './sphinx.conf')
|
114
|
+
paths.map!{|path| Pathname.new(path).expand_path}
|
115
|
+
|
116
|
+
@config = paths.find{|path| path.readable? && `#{indexer_bin} --config #{path}` !~ /fatal|error/i}
|
117
|
+
@config ? ConfigParser.parse(@config) : {}
|
118
|
+
end
|
119
|
+
end # Config
|
120
|
+
end # Sphinx
|
121
|
+
end # Adapters
|
122
|
+
end # DataMapper
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'extlib', '~> 0.9.7'
|
4
|
+
require 'extlib'
|
5
|
+
|
6
|
+
require 'pathname'
|
7
|
+
require 'strscan'
|
8
|
+
|
9
|
+
module DataMapper
|
10
|
+
module Adapters
|
11
|
+
module Sphinx
|
12
|
+
module ConfigParser
|
13
|
+
extend Extlib::Assertions
|
14
|
+
|
15
|
+
# Parse a sphinx config file and return searchd options as a hash.
|
16
|
+
#
|
17
|
+
# ==== Raises
|
18
|
+
# RuntimeError:: On parsing errors.
|
19
|
+
#
|
20
|
+
# ==== Parameters
|
21
|
+
# path<String>:: Sphinx configuration file.
|
22
|
+
#
|
23
|
+
# ==== Returns
|
24
|
+
# Hash:: Searchd options.
|
25
|
+
def self.parse(path)
|
26
|
+
assert_kind_of 'path', path, Pathname, String
|
27
|
+
|
28
|
+
config = Pathname(path).read
|
29
|
+
config.gsub!(/\r\n|\r|\n/, "\n") # Everything in \n
|
30
|
+
config.gsub!(/\s*\\\n\s*/, ' ') # Remove unixy line wraps.
|
31
|
+
|
32
|
+
blocks(StringScanner.new(config), out = [])
|
33
|
+
out.find{|c| c['type'] =~ /searchd/i} || {}
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
def self.blocks(conf, out = []) #:nodoc:
|
38
|
+
if conf.scan(/\#[^\n]*\n/) || conf.scan(/\s+/)
|
39
|
+
blocks(conf, out)
|
40
|
+
elsif conf.scan(/indexer|searchd|source|index/i)
|
41
|
+
out << group = {'type' => conf.matched}
|
42
|
+
if conf.matched =~ /^(?:index|source)$/i
|
43
|
+
conf.scan(/\s* ([\w_\-]+) (?:\s*:\s*([\w_\-]+))? \s*/x) or raise "Expected #{group[:type]} name."
|
44
|
+
group['name'] = conf[1]
|
45
|
+
group['ancestor'] = conf[2]
|
46
|
+
end
|
47
|
+
conf.scan(/\s*\{/) or raise %q{Expected '\{'.}
|
48
|
+
pairs(conf, kv = {})
|
49
|
+
group.merge!(kv)
|
50
|
+
conf.scan(/\s*\}/) or raise %q{Expected '\}'.}
|
51
|
+
blocks(conf, out)
|
52
|
+
else
|
53
|
+
raise "Unknown near: #{conf.peek(30)}" unless conf.eos?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.pairs(conf, out = {}) #:nodoc:
|
58
|
+
if conf.scan(/\#[^\n]*\n/) || conf.scan(/\s+/)
|
59
|
+
pairs(conf, out)
|
60
|
+
elsif conf.scan(/[\w_-]+/)
|
61
|
+
key = conf.matched
|
62
|
+
conf.scan(/\s*=/) or raise %q{Expected '='.}
|
63
|
+
out[key] = conf.scan(/[^\n]*\n/).strip
|
64
|
+
pairs(conf, out)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end # ConfigParser
|
68
|
+
end # Sphinx
|
69
|
+
end # Adapters
|
70
|
+
end # DataMapper
|
71
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
module Sphinx
|
4
|
+
|
5
|
+
# Sphinx index definition.
|
6
|
+
class Index
|
7
|
+
include Assertions
|
8
|
+
|
9
|
+
# Options.
|
10
|
+
attr_reader :model, :name, :options
|
11
|
+
|
12
|
+
# ==== Parameters
|
13
|
+
# model<DataMapper::Model>:: Your resources model.
|
14
|
+
# name<Symbol, String>:: The index name.
|
15
|
+
# options<Hash>:: Optional arguments.
|
16
|
+
#
|
17
|
+
# ==== Options
|
18
|
+
# :delta<Boolean>::
|
19
|
+
# Delta index. Delta indexes will be searched last when multiple indexes are defined for a
|
20
|
+
# resource. Default is false.
|
21
|
+
def initialize(model, name, options = {})
|
22
|
+
assert_kind_of 'model', model, Model
|
23
|
+
assert_kind_of 'name', name, Symbol, String
|
24
|
+
assert_kind_of 'options', options, Hash
|
25
|
+
|
26
|
+
@model = model
|
27
|
+
@name = name.to_sym
|
28
|
+
@delta = options.fetch(:delta, false)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Is the index a delta index.
|
32
|
+
def delta?
|
33
|
+
!!@delta
|
34
|
+
end
|
35
|
+
end # Index
|
36
|
+
end # Sphinx
|
37
|
+
end # Adapters
|
38
|
+
end # DataMapper
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Adapters
|
3
|
+
module Sphinx
|
4
|
+
|
5
|
+
# Sphinx extended search query string from DataMapper query.
|
6
|
+
class Query
|
7
|
+
include Extlib::Assertions
|
8
|
+
|
9
|
+
# Initialize a new extended Sphinx query from a DataMapper::Query object.
|
10
|
+
#
|
11
|
+
# If the query has no conditions an '' empty string will be generated possibly triggering Sphinx's full scan
|
12
|
+
# mode.
|
13
|
+
#
|
14
|
+
# ==== See
|
15
|
+
# * http://www.sphinxsearch.com/doc.html#searching
|
16
|
+
# * http://www.sphinxsearch.com/doc.html#conf-docinfo
|
17
|
+
# * http://www.sphinxsearch.com/doc.html#extended-syntax
|
18
|
+
#
|
19
|
+
# ==== Raises
|
20
|
+
# NotImplementedError:: DataMapper operators that can't be expressed in the extended sphinx query syntax.
|
21
|
+
#
|
22
|
+
# ==== Parameters
|
23
|
+
# query<DataMapper::Query>:: DataMapper query object.
|
24
|
+
def initialize(query)
|
25
|
+
assert_kind_of 'query', query, DataMapper::Query
|
26
|
+
@query = []
|
27
|
+
|
28
|
+
if query.conditions.empty?
|
29
|
+
@query << ''
|
30
|
+
else
|
31
|
+
query.conditions.each do |operator, property, value|
|
32
|
+
next if property.kind_of? Sphinx::Attribute # Filters are added elsewhere.
|
33
|
+
normalized = normalize_value(value)
|
34
|
+
@query << case operator
|
35
|
+
when :eql, :like then '@%s "%s"' % [property.field.to_s, normalized.join(' ')]
|
36
|
+
when :not then '@%s -"%s"' % [property.field.to_s, normalized.join(' ')]
|
37
|
+
when :in then '@%s (%s)' % [property.field.to_s, normalized.map{|v| %{"#{v}"}}.join(' | ')]
|
38
|
+
when :raw then "#{property}"
|
39
|
+
else raise NotImplementedError.new("Sphinx: Query fields do not support the #{operator} operator")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# ==== Returns
|
46
|
+
# String:: The extended sphinx query string.
|
47
|
+
def to_s
|
48
|
+
@query.join(' ')
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
# Normalize and escape DataMapper query value(s) to escaped sphinx query values.
|
53
|
+
#
|
54
|
+
# ==== Parameters
|
55
|
+
# value<String, Array>:: The query value.
|
56
|
+
#
|
57
|
+
# ==== Returns
|
58
|
+
# Array:: An array of one or more query values.
|
59
|
+
def normalize_value(value)
|
60
|
+
[value].flatten.map do |v|
|
61
|
+
v.to_s.gsub(/[\(\)\|\-!@~"&\/]/){|char| "\\#{char}"}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end # Query
|
65
|
+
end # Sphinx
|
66
|
+
end # Adapters
|
67
|
+
end # DataMapper
|