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.
Files changed (41) hide show
  1. data/History.txt +12 -0
  2. data/Manifest.txt +24 -13
  3. data/README.txt +44 -28
  4. data/Rakefile +1 -1
  5. data/dm-sphinx-adapter.gemspec +7 -7
  6. data/lib/dm-sphinx-adapter.rb +8 -6
  7. data/lib/dm-sphinx-adapter/adapter.rb +218 -0
  8. data/lib/dm-sphinx-adapter/attribute.rb +38 -0
  9. data/lib/dm-sphinx-adapter/client.rb +109 -0
  10. data/lib/dm-sphinx-adapter/config.rb +122 -0
  11. data/lib/dm-sphinx-adapter/config_parser.rb +71 -0
  12. data/lib/dm-sphinx-adapter/index.rb +38 -0
  13. data/lib/dm-sphinx-adapter/query.rb +67 -0
  14. data/lib/dm-sphinx-adapter/resource.rb +103 -0
  15. data/test/{fixtures/item.sql → files/dm_sphinx_adapter_test.sql} +3 -3
  16. data/test/{fixtures/item_resource_explicit.rb → files/resource_explicit.rb} +5 -6
  17. data/test/{fixtures/item_resource_only.rb → files/resource_resource.rb} +4 -5
  18. data/test/{fixtures/item.rb → files/resource_searchable.rb} +8 -5
  19. data/test/files/resource_storage_name.rb +11 -0
  20. data/test/files/resource_vanilla.rb +7 -0
  21. data/test/{data → files}/sphinx.conf +11 -6
  22. data/test/test_adapter.rb +38 -0
  23. data/test/test_adapter_explicit.rb +48 -0
  24. data/test/test_adapter_resource.rb +25 -0
  25. data/test/test_adapter_searchable.rb +23 -0
  26. data/test/test_adapter_vanilla.rb +46 -0
  27. data/test/test_client.rb +9 -21
  28. data/test/test_config.rb +58 -24
  29. data/test/test_config_parser.rb +29 -0
  30. data/test/test_query.rb +47 -0
  31. data/test/test_type_attribute.rb +8 -0
  32. data/test/test_type_index.rb +8 -0
  33. metadata +38 -19
  34. data/lib/dm-sphinx-adapter/sphinx_adapter.rb +0 -220
  35. data/lib/dm-sphinx-adapter/sphinx_attribute.rb +0 -22
  36. data/lib/dm-sphinx-adapter/sphinx_client.rb +0 -81
  37. data/lib/dm-sphinx-adapter/sphinx_config.rb +0 -160
  38. data/lib/dm-sphinx-adapter/sphinx_index.rb +0 -21
  39. data/lib/dm-sphinx-adapter/sphinx_resource.rb +0 -88
  40. data/test/helper.rb +0 -7
  41. 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