dm-sphinx-adapter 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,12 @@
1
+ === 0.3 / 2008-11-18
2
+
3
+ * Removed calls to indexer on create/update. See README.txt
4
+ * Made the client object available from the adapter.
5
+
6
+ === 0.2 / 2008-11-09
7
+
8
+ * Addributes.
9
+ * Self managed searchd daemon if you want it.
10
+
11
+ === 0.1 / 2008-10-24
12
+
data/LICENCE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Shane Hanna
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,20 @@
1
+ History.txt
2
+ LICENCE.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ dm-sphinx-adapter.gemspec
7
+ lib/dm-sphinx-adapter.rb
8
+ lib/dm-sphinx-adapter/sphinx_adapter.rb
9
+ lib/dm-sphinx-adapter/sphinx_attribute.rb
10
+ lib/dm-sphinx-adapter/sphinx_client.rb
11
+ lib/dm-sphinx-adapter/sphinx_config.rb
12
+ lib/dm-sphinx-adapter/sphinx_index.rb
13
+ lib/dm-sphinx-adapter/sphinx_resource.rb
14
+ test/data/sphinx.conf
15
+ test/fixtures/item.rb
16
+ test/fixtures/item.sql
17
+ test/helper.rb
18
+ test/test_client.rb
19
+ test/test_config.rb
20
+ test/test_search.rb
data/README.txt ADDED
@@ -0,0 +1,192 @@
1
+ = DataMapper Sphinx Adapter
2
+
3
+ A Sphinx DataMapper adapter.
4
+
5
+ == Synopsis
6
+
7
+ DataMapper uses URIs or a connection has to connect to your data-stores. In this case the sphinx search daemon
8
+ <tt>searchd</tt>.
9
+
10
+ On its own this adapter will only return an array of document IDs when queried. The dm-more source (not the gem)
11
+ however provides dm-is-searchable, a common interface to search one adapter and load documents from another. My
12
+ suggestion is to use this adapter in tandem with dm-is-searchable.
13
+
14
+ The dm-is-searchable plugin is part of dm-more though unfortunately isn't built and bundled with dm-more gem.
15
+ You'll need to checkout the dm-more source with Git from git://github.com/sam/dm-more.git and build/install the
16
+ gem yourself.
17
+
18
+ git clone git://github.com/sam/dm-more.git
19
+ cd dm-more/dm-is-searchable
20
+ sudo rake install_gem
21
+
22
+ Like all DataMapper adapters you can connect with a Hash or URI.
23
+
24
+ A URI:
25
+ DataMapper.setup(:search, 'sphinx://localhost')
26
+
27
+ The breakdown is:
28
+ "#{adapter}://#{host}:#{port}/#{config}"
29
+ - adapter Must be :sphinx
30
+ - host Hostname (default: localhost)
31
+ - port Optional port number (default: 3312)
32
+ - config Optional but strongly recommended path to sphinx config file.
33
+
34
+ Alternatively supply a Hash:
35
+ DataMapper.setup(:search, {
36
+ :adapter => 'sphinx', # required
37
+ :config => './sphinx.conf' # optional. Recommended though.
38
+ :host => 'localhost', # optional. Default: localhost
39
+ :port => 3312 # optional. Default: 3312
40
+ :managed => true # optional. Self managed searchd server using daemon_controller.
41
+ }
42
+
43
+ === DataMapper
44
+
45
+ require 'rubygems'
46
+ require 'dm-sphinx-adapter'
47
+
48
+ DataMapper.setup(:default, 'sqlite3::memory:')
49
+ DataMapper.setup(:search, 'sphinx://localhost:3312')
50
+
51
+ class Item
52
+ include DataMapper::Resource
53
+ property :id, Serial
54
+ property :name, String
55
+ end
56
+
57
+ # Fire up your sphinx search daemon and start searching.
58
+ docs = repository(:search){ Item.all(:name => 'barney') } # Search 'items' index for '@name barney'
59
+ ids = docs.map{|doc| doc[:id]}
60
+ items = Item.all(:id => ids) # Search :default for all the document id's returned by sphinx.
61
+
62
+ === DataMapper and Is Searchable
63
+
64
+ require 'rubygems'
65
+ require 'dm-core'
66
+ require 'dm-is-searchable'
67
+ require 'dm-sphinx-adapter'
68
+
69
+ # Connections.
70
+ DataMapper.setup(:default, 'sqlite3::memory:')
71
+ DataMapper.setup(:search, 'sphinx://localhost:3312')
72
+
73
+ class Item
74
+ include DataMapper::Resource
75
+ property :id, Serial
76
+ property :name, String
77
+
78
+ is :searchable # defaults to :search repository though you can be explicit:
79
+ # is :searchable, :repository => :sphinx
80
+ end
81
+
82
+ # Fire up your sphinx search daemon and start searching.
83
+ items = Item.search(:name => 'barney') # Search 'items' index for '@name barney'
84
+
85
+ === Merb, DataMapper and Is Searchable
86
+
87
+ # config/init.rb
88
+ dependency 'dm-is-searchable'
89
+ dependency 'dm-sphinx-adapter'
90
+
91
+ # config/database.yml
92
+ ---
93
+ development: &defaults
94
+ repositories:
95
+ search:
96
+ adapter: sphinx
97
+ host: localhost
98
+ port: 3312
99
+
100
+ # app/models/item.rb
101
+ class Item
102
+ include DataMapper::Resource
103
+ property :id, Serial
104
+ property :name, String
105
+
106
+ is :searchable # defaults to :search repository though you can be explicit:
107
+ # is :searchable, :repository => :sphinx
108
+ end # Item
109
+
110
+ # Fire up your sphinx search daemon and start searching.
111
+ Item.search(:name => 'barney') # Search 'items' index for '@name barney'
112
+
113
+ === DataMapper::SphinxResource
114
+
115
+ For finer grained control you can include DataMapper::SphinxResource. For instance you can search one or more indexes
116
+ and sort, include or exclude by attributes defined in your sphinx configuration:
117
+
118
+ class Item
119
+ include DataMapper::Resource # Optional, included by SphinxResource if you leave it out yourself.
120
+ include DataMapper::SphinxResource
121
+ property :id, Serial
122
+ property :name, String
123
+
124
+ is :searchable
125
+ repository(:search) do
126
+ index :items
127
+ index :items_delta, :delta => true
128
+
129
+ # Sphinx attributes to sort include/exclude by.
130
+ attribute :updated_on, DateTime
131
+ end
132
+
133
+ end # Item
134
+
135
+ # Search 'items, items_delta' index for '@name barney' updated in the last 30 minutes.
136
+ Item.search(:name => 'barney', :updated => (Time.now - 1800 .. Time.now))
137
+
138
+ == Sphinx Configuration.
139
+
140
+ Though you don't have to supply the sphinx configuration file to dm-sphinx-adapter I'd recommend doing it anyway.
141
+ It's more DRY since all searchd/indexer options can be read straight from the configuration.
142
+
143
+ DataMapper.setup(:search, :adapter => 'sphinx', :config => '/path/to/sphinx.conf')
144
+ DataMapper.setup(:search, 'sphinx://localhost/path/to/sphinx.conf')
145
+
146
+ If your sphinx.conf lives in either of the default locations /usr/local/etc/sphinx.conf or ./sphinx.conf then you
147
+ only need to supply:
148
+
149
+ DataMapper.setup(:search, :adapter => 'sphinx')
150
+
151
+ == Searchd
152
+
153
+ As of 0.2 I've added a managed searchd option using daemon_controller. It may come in handy if you only use God, Monit
154
+ or whatever in production. Use the Hash form of DataMapper#setup and supply the option :managed with a true value and
155
+ daemon_controller will start searchd on demand.
156
+
157
+ It is already strongly encouraged but you will need to specify the path to your sphinx configuration file in order for
158
+ searchd to run. See Sphinx Configuration, DataMapper::SphinxManagedClient.
159
+
160
+ The daemon_controller library can be found only on github, not rubyforge.
161
+ See http://github.com/FooBarWidget/daemon_controller/tree/master
162
+
163
+ == Indexer and Live(ish) updates.
164
+
165
+ As of 0.3 the indexer will no longer be fired on create/update even if you have delta indexes defined. Sphinx indexing
166
+ is blazing fast but unless your resource sees very little activity you will run the risk of lock errors on
167
+ the temporary delta index files (.tmpl.sp1) and your delta index won't be updated. Given this functionality is
168
+ unreliable at best I've chosen to remove it.
169
+
170
+ For reliable live(ish) updates in a main + delta scheme it's probably best you schedule them outside of your ORM.
171
+ Andrew (Shodan) Aksyonoff of Sphinx suggests a cronjob or alternatively if you need even less lag to "run indexer in
172
+ an endless loop, with a few seconds of sleep in between to allow searchd some headroom to pick up the changes".
173
+
174
+ == Todo
175
+
176
+ * Tests. Clearly I only test what I see as important or broken which drives TDD people crazy sometimes :)
177
+ * Loads of documentation. Most of it is unchecked YARD at the moment.
178
+ * Add DataMapper::SphinxClient#attribute_set to allow attribute modification on one or more indexes. It's the only
179
+ thing missing if you understand the pitfalls and still want to add thinking-sphinx like delta indexing to your
180
+ resource.
181
+
182
+ == Dependencies
183
+
184
+ dm-core and riddle are technically the only requirements though I'd recommend using the dm-more plugin dm-is-searchable
185
+ instead of fetching the document id's yourself.
186
+
187
+ Unfortunately dm-is-searchable isn't installed even when you build the dm-more gem from github master. You'll need to
188
+ build and install the gem yourself from source.
189
+
190
+ == Contributing
191
+
192
+ Go nuts. Just send me a pull request (github or otherwise) when you are happy with your code.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.new('dm-sphinx-adapter', '0.3') do |p|
7
+ p.developer('Shane Hanna', 'shane.hanna@gmail.com')
8
+ p.extra_deps = [
9
+ ['dm-core', '~> 0.9.7'],
10
+ ['riddle', '~> 0.9']
11
+ ]
12
+ end
13
+
14
+ # http://blog.behindlogic.com/2008/10/auto-generate-your-manifest-and-gemspec.html
15
+ desc 'Rebuild manifest and gemspec.'
16
+ task :cultivate do
17
+ Dir.chdir(File.dirname(__FILE__)) do #TODO: Is this required?
18
+ system %q{git ls-files | grep -v "\.gitignore" > Manifest.txt}
19
+ system %q{rake debug_gem | grep -v "(in " > `basename \`pwd\``.gemspec}
20
+ end
21
+ end
22
+
23
+ # vim: syntax=Ruby
@@ -0,0 +1,39 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{dm-sphinx-adapter}
3
+ s.version = "0.3"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Shane Hanna"]
7
+ s.date = %q{2008-11-18}
8
+ s.description = %q{}
9
+ s.email = ["shane.hanna@gmail.com"]
10
+ s.extra_rdoc_files = ["History.txt", "LICENCE.txt", "Manifest.txt", "README.txt"]
11
+ s.files = ["History.txt", "LICENCE.txt", "Manifest.txt", "README.txt", "Rakefile", "dm-sphinx-adapter.gemspec", "lib/dm-sphinx-adapter.rb", "lib/dm-sphinx-adapter/sphinx_adapter.rb", "lib/dm-sphinx-adapter/sphinx_attribute.rb", "lib/dm-sphinx-adapter/sphinx_client.rb", "lib/dm-sphinx-adapter/sphinx_config.rb", "lib/dm-sphinx-adapter/sphinx_index.rb", "lib/dm-sphinx-adapter/sphinx_resource.rb", "test/data/sphinx.conf", "test/fixtures/item.rb", "test/fixtures/item.sql", "test/helper.rb", "test/test_client.rb", "test/test_config.rb", "test/test_search.rb"]
12
+ s.has_rdoc = true
13
+ s.homepage = %q{A Sphinx DataMapper adapter.}
14
+ s.rdoc_options = ["--main", "README.txt"]
15
+ s.require_paths = ["lib"]
16
+ s.rubyforge_project = %q{dm-sphinx-adapter}
17
+ s.rubygems_version = %q{1.2.0}
18
+ s.summary = %q{}
19
+ s.test_files = ["test/test_client.rb", "test/test_config.rb", "test/test_search.rb"]
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 2
24
+
25
+ if current_version >= 3 then
26
+ s.add_runtime_dependency(%q<dm-core>, ["~> 0.9.7"])
27
+ s.add_runtime_dependency(%q<riddle>, ["~> 0.9"])
28
+ s.add_development_dependency(%q<hoe>, [">= 1.8.2"])
29
+ else
30
+ s.add_dependency(%q<dm-core>, ["~> 0.9.7"])
31
+ s.add_dependency(%q<riddle>, ["~> 0.9"])
32
+ s.add_dependency(%q<hoe>, [">= 1.8.2"])
33
+ end
34
+ else
35
+ s.add_dependency(%q<dm-core>, ["~> 0.9.7"])
36
+ s.add_dependency(%q<riddle>, ["~> 0.9"])
37
+ s.add_dependency(%q<hoe>, [">= 1.8.2"])
38
+ end
39
+ end
@@ -0,0 +1,220 @@
1
+ require 'benchmark'
2
+
3
+ # TODO: I think perhaps I should move all the query building code to a lib of its own.
4
+
5
+ module DataMapper
6
+ module Adapters
7
+
8
+ # == Synopsis
9
+ # DataMapper uses URIs or a connection has to connect to your data-stores. In this case the sphinx search daemon
10
+ # <tt>searchd</tt>.
11
+ #
12
+ # On its own this adapter will only return an array of document IDs when queried. The dm-more source (not the gem)
13
+ # however provides dm-is-searchable, a common interface to search one adapter and load documents from another. My
14
+ # suggestion is to use this adapter in tandem with dm-is-searchable.
15
+ #
16
+ # The dm-is-searchable plugin is part of dm-more though unfortunately isn't built and bundled with dm-more gem.
17
+ # You'll need to checkout the dm-more source with Git from git://github.com/sam/dm-more.git and build/install the
18
+ # gem yourself.
19
+ #
20
+ # git clone git://github.com/sam/dm-more.git
21
+ # cd dm-more/dm-is-searchable
22
+ # sudo rake install_gem
23
+ #
24
+ # Like all DataMapper adapters you can connect with a Hash or URI.
25
+ #
26
+ # A URI:
27
+ # DataMapper.setup(:search, 'sphinx://localhost')
28
+ #
29
+ # The breakdown is:
30
+ # "#{adapter}://#{host}:#{port}/#{config}"
31
+ # - adapter Must be :sphinx
32
+ # - host Hostname (default: localhost)
33
+ # - port Optional port number (default: 3312)
34
+ # - config Optional but recommended path to sphinx config file.
35
+ #
36
+ # Alternatively supply a Hash:
37
+ # DataMapper.setup(:search, {
38
+ # :adapter => 'sphinx', # required
39
+ # :config => './sphinx.conf' # optional. Recommended though.
40
+ # :host => 'localhost', # optional. Default: localhost
41
+ # :port => 3312 # optional. Default: 3312
42
+ # :managed => true # optional. Self managed searchd server using daemon_controller.
43
+ # })
44
+ class SphinxAdapter < AbstractAdapter
45
+ ##
46
+ # Initialize the sphinx adapter.
47
+ #
48
+ # @param [URI, DataObject::URI, Addressable::URI, String, Hash, Pathname] uri_or_options
49
+ # @see DataMapper::SphinxConfig
50
+ # @see DataMapper::SphinxClient
51
+ def initialize(name, uri_or_options)
52
+ super
53
+
54
+ managed = !!(uri_or_options.kind_of?(Hash) && uri_or_options[:managed])
55
+ @client = managed ? SphinxManagedClient.new(uri_or_options) : SphinxClient.new(uri_or_options)
56
+ end
57
+
58
+ ##
59
+ # Interaction with searchd and indexer.
60
+ #
61
+ # @see DataMapper::SphinxClient
62
+ # @see DataMapper::SphinxManagedClient
63
+ attr_reader :client
64
+
65
+ def create(resources) #:nodoc:
66
+ true
67
+ end
68
+
69
+ def delete(query) #:nodoc:
70
+ true
71
+ end
72
+
73
+ def read_many(query)
74
+ read(query)
75
+ end
76
+
77
+ def read_one(query)
78
+ read(query).first
79
+ end
80
+
81
+ protected
82
+ ##
83
+ # List sphinx indexes to search.
84
+ # If no indexes are explicitly declared using DataMapper::SphinxResource then the tableized model name is used.
85
+ #
86
+ # @see DataMapper::SphinxResource#sphinx_indexes
87
+ def indexes(model)
88
+ indexes = model.sphinx_indexes(repository(self.name).name) if model.respond_to?(:sphinx_indexes)
89
+ if indexes.nil? or indexes.empty?
90
+ # TODO: Is it resource_naming_convention.call(model.name) ?
91
+ indexes = [SphinxIndex.new(model, Extlib::Inflection.tableize(model.name))]
92
+ end
93
+ indexes
94
+ end
95
+
96
+ ##
97
+ # List sphinx delta indexes to search.
98
+ #
99
+ # @see DataMapper::SphinxResource#sphinx_indexes
100
+ def delta_indexes(model)
101
+ indexes(model).find_all{|i| i.delta?}
102
+ end
103
+
104
+ ##
105
+ # Query sphinx for a list of document IDs.
106
+ #
107
+ # @param [DataMapper::Query]
108
+ def read(query)
109
+ from = indexes(query.model).map{|index| index.name}.join(', ')
110
+ search = search_query(query)
111
+ options = {
112
+ :match_mode => :extended, # TODO: Modes!
113
+ :limit => (query.limit ? query.limit.to_i : 0),
114
+ :offset => (query.offset ? query.offset.to_i : 0),
115
+ :filters => search_filters(query) # By attribute.
116
+ }
117
+ if order = search_order(query)
118
+ options.update(
119
+ :sort_mode => :extended,
120
+ :sort_by => order
121
+ )
122
+ end
123
+
124
+ res = @client.search(search, from, options)
125
+ raise res[:error] unless res[:error].nil?
126
+
127
+ DataMapper.logger.info(
128
+ %q{Sphinx (%.3f): search '%s' in '%s' found %d documents} % [res[:time], search, from, res[:total]]
129
+ )
130
+ res[:matches].map{|doc| doc[:doc]}
131
+ end
132
+
133
+ ##
134
+ # Sphinx search query string from properties (fields).
135
+ #
136
+ # If the query has no conditions an '' empty string will be generated possibly triggering Sphinx's full scan
137
+ # mode.
138
+ #
139
+ # @see http://www.sphinxsearch.com/doc.html#searching
140
+ # @see http://www.sphinxsearch.com/doc.html#conf-docinfo
141
+ # @param [DataMapper::Query]
142
+ # @return [String]
143
+ def search_query(query)
144
+ match = []
145
+
146
+ if query.conditions.empty?
147
+ match << ''
148
+ else
149
+ # TODO: This needs to be altered by match mode since not everything is supported in different match modes.
150
+ query.conditions.each do |operator, property, value|
151
+ next if property.kind_of? SphinxAttribute # Filters are added elsewhere.
152
+ # TODO: Why does my gem riddle differ from the vendor riddle that comes with ts?
153
+ # escaped_value = Riddle.escape(value)
154
+ escaped_value = value.to_s.gsub(/[\(\)\|\-!@~"&\/]/){|char| "\\#{char}"}
155
+ match << case operator
156
+ when :eql, :like then "@#{property.field} #{escaped_value}"
157
+ when :not then "@#{property.field} -#{escaped_value}"
158
+ when :lt, :gt, :lte, :gte
159
+ DataMapper.logger.warn('Sphinx: Query properties with lt, gt, lte, gte are treated as .eql')
160
+ "@#{name} #{escaped_value}"
161
+ when :raw
162
+ "#{property}"
163
+ end
164
+ end
165
+ end
166
+ match.join(' ')
167
+ end
168
+
169
+ ##
170
+ # Sphinx search query filters from attributes.
171
+ # @param [DataMapper::Query]
172
+ # @return [Array]
173
+ def search_filters(query)
174
+ filters = []
175
+ query.conditions.each do |operator, attribute, value|
176
+ next unless attribute.kind_of? SphinxAttribute
177
+ # TODO: Value cast to uint, bool, str2ordinal, float
178
+ filters << case operator
179
+ when :eql, :like then Riddle::Client::Filter.new(attribute.name.to_s, filter_value(value))
180
+ when :not then Riddle::Client::Filter.new(attribute.name.to_s, filter_value(value), true)
181
+ else
182
+ error = "Sphinx: Query attributes do not support the #{operator} operator"
183
+ DataMapper.logger.error(error)
184
+ raise error # TODO: RuntimeError subclass and more information about the actual query.
185
+ end
186
+ end
187
+ filters
188
+ end
189
+
190
+ ##
191
+ # Order by attributes.
192
+ #
193
+ # @return [String or Symbol]
194
+ def search_order(query)
195
+ by = []
196
+ # TODO: How do you tell the difference between the default query order and someone explicitly asking for
197
+ # sorting by the primary key?
198
+ query.order.each do |order|
199
+ next unless order.property.kind_of? SphinxAttribute
200
+ by << [order.property.field, order.direction].join(' ')
201
+ end
202
+ by.empty? ? nil : by.join(', ')
203
+ end
204
+
205
+ # TODO: Move this to SphinxAttribute#something.
206
+ # This is ninja'd straight from TS just to get things going.
207
+ def filter_value(value)
208
+ case value
209
+ when Range
210
+ value.first.is_a?(Time) ? value.first.to_i..value.last.to_i : value
211
+ when Array
212
+ value.collect { |val| val.is_a?(Time) ? val.to_i : val }
213
+ else
214
+ Array(value)
215
+ end
216
+ end
217
+ end # SphinxAdapter
218
+ end # Adapters
219
+ end # DataMapper
220
+
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,76 @@
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
+ @config = SphinxConfig.new(uri_or_options)
10
+ end
11
+
12
+ # TODO: What about filters?
13
+ def search(query, indexes = '*', options = {})
14
+ indexes = indexes.join(' ') if indexes.kind_of?(Array)
15
+
16
+ client = Riddle::Client.new(@config.address, @config.port)
17
+ options.each{|k, v| client.method("#{k}=".to_sym).call(v) if client.respond_to?("#{k}=".to_sym)}
18
+ client.query(query, indexes.to_s)
19
+ end
20
+
21
+ ##
22
+ # Index one or more indexes.
23
+ #
24
+ # @param [Array, String] indexes Defaults to --all if indexes is nil or '*'.
25
+ def index(indexes = nil, options = {})
26
+ indexes = indexes.join(' ') if indexes.kind_of?(Array)
27
+
28
+ command = @config.indexer_bin
29
+ command << " --rotate" if running?
30
+ command << ((indexes.nil? || indexes == '*') ? ' --all' : " #{indexes.to_s}")
31
+ warn "Sphinx: Indexer #{$1}" if `#{command}` =~ /(?:error|fatal|warning):?\s*([^\n]+)/i
32
+ end
33
+
34
+ protected
35
+
36
+ ##
37
+ # Is the client running.
38
+ #
39
+ # Tests the address and port set in the configuration file.
40
+ def running?
41
+ !!TCPSocket.new(@config.address, @config.port) rescue nil
42
+ end
43
+ end # SphinxClient
44
+
45
+ ##
46
+ # Managed searchd if you don't already have god/monit doing the job for you.
47
+ #
48
+ # Requires you have daemon_controller installed.
49
+ # @see http://github.com/FooBarWidget/daemon_controller/tree/master
50
+ class SphinxManagedClient < SphinxClient
51
+ def initialize(url_or_options)
52
+ super
53
+
54
+ # Fire up searchd.
55
+ require 'daemon_controller'
56
+ @client = DaemonController.new(
57
+ :identifier => 'Sphinx searchd',
58
+ :start_command => @config.searchd_bin,
59
+ :stop_command => "#{@config.searchd_bin} --stop",
60
+ :ping_command => method(:running?),
61
+ :pid_file => @config.pid_file,
62
+ :log_file => @config.log
63
+ )
64
+ end
65
+
66
+ def search(*args)
67
+ @client.connect do
68
+ super *args
69
+ end
70
+ end
71
+
72
+ def stop
73
+ @client.stop
74
+ end
75
+ end # SphinxManagedClient
76
+ end # DataMapper
@@ -0,0 +1,160 @@
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
+
18
+ path = case uri_or_options
19
+ when Addressable::URI, DataObjects::URI then uri_or_options.path
20
+ when Hash then uri_or_options[:config] || uri_or_options[:database]
21
+ when Pathname then uri_or_options
22
+ when String then DataObjects::URI.parse(uri_or_options).path
23
+ end
24
+ parse('' + path.to_s) # Force stringy since Pathname#to_s is broken IMO.
25
+ end
26
+
27
+ ##
28
+ # Configuration file full path name.
29
+ #
30
+ # @return [String]
31
+ def config
32
+ @config
33
+ end
34
+
35
+ ##
36
+ # Indexer binary full path name and config argument.
37
+ def indexer_bin(use_config = true)
38
+ path = 'indexer' # TODO: Real.
39
+ path << " --config #{config}" if config
40
+ path
41
+ end
42
+
43
+ ##
44
+ # Searchd binardy full path name and config argument.
45
+ def searchd_bin(use_config = true)
46
+ path = 'searchd' # TODO: Real.
47
+ path << " --config #{config}" if config
48
+ path
49
+ end
50
+
51
+ ##
52
+ # Searchd address.
53
+ def address
54
+ searchd['address']
55
+ end
56
+
57
+ ##
58
+ # Searchd port.
59
+ def port
60
+ searchd['port']
61
+ end
62
+
63
+ ##
64
+ # Searchd pid_file.
65
+ def pid_file
66
+ searchd['pid_file'] or raise "Mandatory pid_file option missing from searchd configuration."
67
+ end
68
+
69
+ ##
70
+ # Searchd log file.
71
+ def log
72
+ searchd['log']
73
+ end
74
+
75
+ ##
76
+ # Searchd configuration options.
77
+ #
78
+ # Defaults will be applied but no validation is done.
79
+ #
80
+ # @see http://www.sphinxsearch.com/doc.html#confgroup-searchd
81
+ def searchd
82
+ unless @searchd
83
+ searchd = @blocks.find{|c| c['type'] =~ /searchd/i} || {}
84
+ @searchd = {
85
+ 'address' => '0.0.0.0',
86
+ 'log' => 'searchd.log',
87
+ 'max_children' => 0,
88
+ 'max_matches' => 1000,
89
+ 'pid_file' => nil,
90
+ 'port' => 3312,
91
+ 'preopen_indexes' => 0,
92
+ 'query_log' => '',
93
+ 'read_timeout' => 5,
94
+ 'seamless_rotate' => 1,
95
+ 'unlink_old' => 1
96
+ }.update(searchd)
97
+ end
98
+ @searchd
99
+ end
100
+
101
+ protected
102
+
103
+ ##
104
+ # Parse a sphinx config file.
105
+ #
106
+ # @param [String] path Searches path, ./path, /path, /usr/local/etc/sphinx.conf, ./sphinx.conf in that order.
107
+ def parse(path = '')
108
+ paths = [
109
+ path,
110
+ path.gsub(%r{^/}, './'),
111
+ path.gsub(%r{^\./}, '/'),
112
+ '/usr/local/etc/sphinx.conf', # TODO: Does this one depend on where searchd/indexer is installed?
113
+ './sphinx.conf'
114
+ ]
115
+ paths.find do |path|
116
+ @config = Pathname.new(path).expand_path
117
+ @config.readable? && `#{indexer_bin}` !~ /fatal|error/i
118
+ end or raise IOError, %{No readable config file (looked in #{paths.join(', ')})}
119
+
120
+ source = config.read
121
+ source.gsub!(/\r\n|\r|\n/, "\n") # Everything in \n
122
+ source.gsub!(/\s*\\\n\s*/, ' ') # Remove unixy line wraps.
123
+ @in = StringScanner.new(source)
124
+ blocks(@blocks = [])
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
@@ -0,0 +1,21 @@
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
@@ -0,0 +1,88 @@
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
+
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+
3
+ # TODO: Hide the shitload of dm-core warnings or at least try to?
4
+ $VERBOSE = nil
5
+ gem 'dm-core', '~> 0.9.7'
6
+ require 'dm-core'
7
+
8
+ # TODO: I think I might move everything to DataMapper::Sphinx::* and ignore the default naming convention.
9
+ require 'pathname'
10
+ dir = Pathname(__FILE__).dirname.expand_path / 'dm-sphinx-adapter'
11
+ require dir / 'sphinx_config'
12
+ require dir / 'sphinx_client'
13
+ require dir / 'sphinx_adapter'
14
+ require dir / 'sphinx_index'
15
+ require dir / 'sphinx_attribute'
16
+ require dir / 'sphinx_resource'
17
+
@@ -0,0 +1,73 @@
1
+ # searchd and indexer must be run from the root directory of this lib.
2
+
3
+ indexer
4
+ {
5
+ mem_limit = 64M
6
+ }
7
+
8
+ searchd
9
+ {
10
+ address = localhost
11
+ port = 3312
12
+ log = test/data/sphinx.log
13
+ query_log = test/data/sphinx.query.log
14
+ read_timeout = 5
15
+ max_children = 30
16
+ pid_file = test/data/sphinx.pid
17
+ max_matches = 1000
18
+ }
19
+
20
+ source items
21
+ {
22
+ type = mysql
23
+ sql_host = localhost
24
+ sql_user = root
25
+ sql_pass =
26
+ sql_db = dm_sphinx_adapter_test
27
+
28
+ sql_query_pre = set names utf8
29
+ sql_query_pre = \
30
+ replace into delta (name, updated_on) ( \
31
+ select 'items', updated_on \
32
+ from items \
33
+ order by updated_on desc \
34
+ limit 1\
35
+ )
36
+
37
+ sql_query = \
38
+ select id, name, likes, unix_timestamp(updated_on) as updated_on \
39
+ from items \
40
+ where updated_on <= ( \
41
+ select updated_on \
42
+ from delta \
43
+ where name = 'items'\
44
+ )
45
+
46
+ sql_query_info = select * from items where id = $id
47
+ sql_attr_timestamp = updated_on
48
+ }
49
+
50
+ source items_delta : items
51
+ {
52
+ sql_query_pre = set names utf8
53
+ sql_query = \
54
+ select id, name, likes, unix_timestamp(updated_on) as updated_on \
55
+ from items \
56
+ where updated_on > ( \
57
+ select updated_on \
58
+ from delta \
59
+ where name = 'items'\
60
+ )
61
+ }
62
+
63
+ index items
64
+ {
65
+ source = items
66
+ path = test/data/sphinx/items
67
+ }
68
+
69
+ index items_delta : items
70
+ {
71
+ source = items_delta
72
+ path = test/data/sphinx/items_delta
73
+ }
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'dm-is-searchable'
3
+ require 'zlib'
4
+
5
+ class Item
6
+ include DataMapper::Resource
7
+ include DataMapper::SphinxResource
8
+
9
+ property :id, Integer, :key => true, :writer => :private
10
+ property :name, String, :nullable => false, :length => 50
11
+ property :likes, Text
12
+ property :updated_on, DateTime
13
+
14
+ is :searchable
15
+ repository(:search) do
16
+ index :items
17
+ index :items_delta, :delta => true
18
+
19
+ # TODO: More attributes.
20
+ attribute :updated, DateTime
21
+ end
22
+
23
+ # I'm using my own (unreleased) Digest::CRC32 DataMapper::Type normally.
24
+ after :name, :set_id
25
+
26
+ protected
27
+ def set_id
28
+ attribute_set(:id, Zlib.crc32(name))
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ drop table if exists delta;
2
+
3
+ create table delta (
4
+ name varchar(50) not null,
5
+ updated_on datetime,
6
+ primary key (name)
7
+ ) engine=innodb default charset=utf8;
8
+
9
+ drop table if exists items;
10
+
11
+ create table items (
12
+ id int(11) not null,
13
+ name varchar(50) not null,
14
+ likes text not null,
15
+ updated_on datetime,
16
+ primary key (id),
17
+ index (updated_on)
18
+ ) engine=innodb default charset=utf8;
19
+
20
+ insert into items (id, name, likes, updated_on) values
21
+ (CRC32('foo'), 'foo', 'I really like foo!', now()),
22
+ (CRC32('bar'), 'bar', 'I really like bar!', now()),
23
+ (CRC32('baz'), 'baz', 'I really like baz!', now());
data/test/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'dm-sphinx-adapter'
2
+ require 'test/fixtures/item'
3
+ require 'test/unit'
4
+
@@ -0,0 +1,43 @@
1
+ require 'helper'
2
+
3
+ class TestClient < Test::Unit::TestCase
4
+ def setup
5
+ @config = Pathname.new(__FILE__).dirname.expand_path / 'data' / 'sphinx.conf'
6
+
7
+ # TODO: A little too brutal for me.
8
+ Dir.chdir(File.join(File.dirname(__FILE__), 'fixtures')) do
9
+ system 'mysql -u root dm_sphinx_adapter_test < item.sql' \
10
+ or raise %q{Tests require the dm_sphinx_adapter_test.items table.}
11
+ end
12
+ end
13
+
14
+ def test_initialize
15
+ assert_nothing_raised do
16
+ DataMapper::SphinxClient.new(@config)
17
+ end
18
+ end
19
+
20
+ def test_index
21
+ client = DataMapper::SphinxClient.new(@config)
22
+ assert_nothing_raised{ client.index }
23
+ assert_nothing_raised{ client.index 'items' }
24
+ assert_nothing_raised{ client.index '*' }
25
+ assert_nothing_raised{ client.index ['items', 'items_delta'] }
26
+ end
27
+
28
+ def test_managed_initialize
29
+ assert_nothing_raised do
30
+ DataMapper::SphinxManagedClient.new(@config)
31
+ end
32
+ end
33
+
34
+ def test_search
35
+ begin
36
+ client = DataMapper::SphinxManagedClient.new(@config)
37
+ client.index
38
+ assert client.search('foo')
39
+ ensure
40
+ client.stop
41
+ end
42
+ end
43
+ end # TestClient
@@ -0,0 +1,41 @@
1
+ require 'helper'
2
+
3
+ class TestConfig < Test::Unit::TestCase
4
+ def setup
5
+ data = Pathname(__FILE__).dirname.expand_path / 'data'
6
+ @config = data / 'sphinx.conf'
7
+ @log = data / 'sphinx.log'
8
+ end
9
+
10
+ def test_initialize
11
+ assert_nothing_raised{ config_new }
12
+ assert_raise(IOError){ config_new(:config => nil) }
13
+ assert_raise(IOError){ config_new(:config => 'blah') }
14
+ assert_raise(IOError){ config_new(:config => @log) }
15
+ end
16
+
17
+ def test_initalize_forms
18
+ assert_nothing_raised{ config_new(:database => @config) }
19
+ # TODO: DataObjects::URI treats /test as the hostname.
20
+ # assert_nothing_raised{ config_new('file://test/data/sphinx.conf') }
21
+ assert_nothing_raised{ config_new('sphinx://localhost/test/data/sphinx.conf') }
22
+ end
23
+
24
+ def test_config
25
+ assert_equal @config, config_new.config
26
+ end
27
+
28
+ def test_searchd
29
+ assert_kind_of Hash, config_new.searchd
30
+ assert_equal 'localhost', config_new.address
31
+ assert_equal '3312', config_new.port
32
+ assert_equal 'test/data/sphinx.pid', config_new.pid_file
33
+ assert_equal 'test/data/sphinx.log', config_new.log
34
+ end
35
+
36
+ protected
37
+ def config_new(options = {:config => @config})
38
+ DataMapper::SphinxConfig.new(options)
39
+ end
40
+
41
+ end # TestConfig
@@ -0,0 +1,37 @@
1
+ require 'helper'
2
+
3
+ class TestSearch < Test::Unit::TestCase
4
+ def setup
5
+ # TODO: A little too brutal for me.
6
+ Dir.chdir(File.join(File.dirname(__FILE__), 'fixtures')) do
7
+ system 'mysql -u root dm_sphinx_adapter_test < item.sql' \
8
+ or raise %q{Tests require the dm_sphinx_adapter_test.items table.}
9
+ end
10
+
11
+ @config = Pathname.new(__FILE__).dirname.expand_path / 'data' / 'sphinx.conf'
12
+ DataMapper.setup(:default, 'mysql://localhost/dm_sphinx_adapter_test')
13
+ DataMapper.setup(:search,
14
+ :adapter => 'sphinx',
15
+ :config => @config,
16
+ :managed => true
17
+ )
18
+ end
19
+
20
+ def teardown
21
+ DataMapper.repository(:search).adapter.client.stop
22
+ # You can also build a new client with the same config and call stop on that.
23
+ # client = DataMapper::SphinxManagedClient.new(@config)
24
+ # client.stop
25
+ end
26
+
27
+ def test_search
28
+ assert_nothing_raised{ Item.search }
29
+ assert_nothing_raised{ Item.search(:name => 'foo') }
30
+ end
31
+
32
+ def test_search_attributes
33
+ assert_nothing_raised do
34
+ Item.search(:updated => (Time.now - 10 .. Time.now + 10))
35
+ end
36
+ end
37
+ end # TestSearch
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dm-sphinx-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.3"
5
+ platform: ruby
6
+ authors:
7
+ - Shane Hanna
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-18 00:00:00 +11:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dm-core
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.7
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: riddle
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: "0.9"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: hoe
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.8.2
44
+ version:
45
+ description: ""
46
+ email:
47
+ - shane.hanna@gmail.com
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - History.txt
54
+ - LICENCE.txt
55
+ - Manifest.txt
56
+ - README.txt
57
+ files:
58
+ - History.txt
59
+ - LICENCE.txt
60
+ - Manifest.txt
61
+ - README.txt
62
+ - Rakefile
63
+ - dm-sphinx-adapter.gemspec
64
+ - lib/dm-sphinx-adapter.rb
65
+ - lib/dm-sphinx-adapter/sphinx_adapter.rb
66
+ - lib/dm-sphinx-adapter/sphinx_attribute.rb
67
+ - lib/dm-sphinx-adapter/sphinx_client.rb
68
+ - lib/dm-sphinx-adapter/sphinx_config.rb
69
+ - lib/dm-sphinx-adapter/sphinx_index.rb
70
+ - lib/dm-sphinx-adapter/sphinx_resource.rb
71
+ - test/data/sphinx.conf
72
+ - test/fixtures/item.rb
73
+ - test/fixtures/item.sql
74
+ - test/helper.rb
75
+ - test/test_client.rb
76
+ - test/test_config.rb
77
+ - test/test_search.rb
78
+ has_rdoc: true
79
+ homepage: A Sphinx DataMapper adapter.
80
+ post_install_message:
81
+ rdoc_options:
82
+ - --main
83
+ - README.txt
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ version:
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ requirements: []
99
+
100
+ rubyforge_project: dm-sphinx-adapter
101
+ rubygems_version: 1.3.0
102
+ signing_key:
103
+ specification_version: 2
104
+ summary: ""
105
+ test_files:
106
+ - test/test_client.rb
107
+ - test/test_config.rb
108
+ - test/test_search.rb