dm-sphinx-adapter 0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt 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