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 +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
|