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
@@ -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
|