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 +12 -0
- data/LICENCE.txt +20 -0
- data/Manifest.txt +20 -0
- data/README.txt +192 -0
- data/Rakefile +23 -0
- data/dm-sphinx-adapter.gemspec +39 -0
- data/lib/dm-sphinx-adapter/sphinx_adapter.rb +220 -0
- data/lib/dm-sphinx-adapter/sphinx_attribute.rb +22 -0
- data/lib/dm-sphinx-adapter/sphinx_client.rb +76 -0
- data/lib/dm-sphinx-adapter/sphinx_config.rb +160 -0
- data/lib/dm-sphinx-adapter/sphinx_index.rb +21 -0
- data/lib/dm-sphinx-adapter/sphinx_resource.rb +88 -0
- data/lib/dm-sphinx-adapter.rb +17 -0
- data/test/data/sphinx.conf +73 -0
- data/test/fixtures/item.rb +30 -0
- data/test/fixtures/item.sql +23 -0
- data/test/helper.rb +4 -0
- data/test/test_client.rb +43 -0
- data/test/test_config.rb +41 -0
- data/test/test_search.rb +37 -0
- metadata +108 -0
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
data/test/test_client.rb
ADDED
@@ -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
|
data/test/test_config.rb
ADDED
@@ -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
|
data/test/test_search.rb
ADDED
@@ -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
|