dm-sphinx-adapter 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
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