dm-groonga-adapter 0.1.0.pre
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/.document +5 -0
- data/.gitignore +26 -0
- data/LICENSE +14 -0
- data/README.rdoc +43 -0
- data/Rakefile +52 -0
- data/VERSION.yml +5 -0
- data/dm-groonga-adapter.gemspec +85 -0
- data/examples/basic.rb +45 -0
- data/lib/groonga_adapter/adapter.rb +207 -0
- data/lib/groonga_adapter/local_index.rb +191 -0
- data/lib/groonga_adapter/model_ext.rb +12 -0
- data/lib/groonga_adapter/remote_index.rb +250 -0
- data/lib/groonga_adapter/remote_result.rb +101 -0
- data/lib/groonga_adapter/repository_ext.rb +13 -0
- data/lib/groonga_adapter/unicode_ext.rb +17 -0
- data/lib/groonga_adapter.rb +9 -0
- data/spec/rcov.opts +6 -0
- data/spec/shared/adapter_example.rb +53 -0
- data/spec/shared/search_example.rb +64 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/specs/adapter_spec.rb +25 -0
- data/spec/specs/remote_result_spec.rb +100 -0
- data/spec/specs/search_spec.rb +26 -0
- metadata +161 -0
data/.document
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
## Github Page
|
2
|
+
_site
|
3
|
+
|
4
|
+
## MAC OS
|
5
|
+
.DS_Store
|
6
|
+
|
7
|
+
## TEXTMATE
|
8
|
+
*.tmproj
|
9
|
+
tmtags
|
10
|
+
|
11
|
+
## EMACS
|
12
|
+
*~
|
13
|
+
\#*
|
14
|
+
.\#*
|
15
|
+
|
16
|
+
## VIM
|
17
|
+
*.swp
|
18
|
+
|
19
|
+
## PROJECT::GENERAL
|
20
|
+
coverage
|
21
|
+
rdoc
|
22
|
+
pkg
|
23
|
+
|
24
|
+
## PROJECT::SPECIFIC
|
25
|
+
#
|
26
|
+
spec/test
|
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Copyright (c) 2009 hiroyuki
|
2
|
+
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
4
|
+
it under the terms of the Gnu Lesser General Public License as
|
5
|
+
published by the Free Software Foundation, either version 2.1 of the
|
6
|
+
License, or any later version.
|
7
|
+
|
8
|
+
This program is distributed in the hope that it will be useful,
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
Gnu Lesser General Public License for more details.
|
12
|
+
|
13
|
+
You should have received a copy of the Gnu Lesser General Public
|
14
|
+
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/README.rdoc
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
= dm-groonga-adapter
|
2
|
+
|
3
|
+
dm-groonga-adapter provides is-search adapter for groonga (http://groonga.org).
|
4
|
+
With dm-is-search, you can use groonga as search repository. Currently, This
|
5
|
+
module supports groonga 0.1.7 or later.
|
6
|
+
|
7
|
+
== Install
|
8
|
+
|
9
|
+
gem install dm-groonga-adapter
|
10
|
+
|
11
|
+
== Dependencies
|
12
|
+
|
13
|
+
* groonga (ruby binding) >= 0.9.1
|
14
|
+
* dm-core >= 0.10.0
|
15
|
+
* dm-more >= 0.10.0
|
16
|
+
|
17
|
+
== Setup Repository
|
18
|
+
|
19
|
+
For a single process site, use groonga dataase files directory.
|
20
|
+
|
21
|
+
DataMapper.setup :search, "groonga:///path/to/database"
|
22
|
+
|
23
|
+
For a multi-process site, use url for a groonga server process.
|
24
|
+
|
25
|
+
DataMapper.setup :search, "groonga://127.0.0.1:10041"
|
26
|
+
|
27
|
+
== Sample Code
|
28
|
+
|
29
|
+
See examples/base.rb and spec/shared/search_spec.rb
|
30
|
+
|
31
|
+
== Note on Patches/Pull Requests
|
32
|
+
|
33
|
+
* Fork the project.
|
34
|
+
* Make your feature addition or bug fix.
|
35
|
+
* Add tests for it. This is important so I don't break it in a
|
36
|
+
future version unintentionally.
|
37
|
+
* Commit, do not mess with rakefile, version, or history.
|
38
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
39
|
+
* Send me a pull request. Bonus points for topic branches.
|
40
|
+
|
41
|
+
== Copyright
|
42
|
+
|
43
|
+
Copyright (c) 2010 hiroyuki. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "dm-groonga-adapter"
|
8
|
+
gem.summary = %Q{datamapper adapter for groonga search engine}
|
9
|
+
gem.description = gem.summary
|
10
|
+
gem.email = "hello@hryk.info"
|
11
|
+
gem.homepage = "http://github.com/hryk/dm-groonga-adapter"
|
12
|
+
gem.authors = ["hiroyuki"]
|
13
|
+
|
14
|
+
gem.add_dependency "groonga", ">= 0.9"
|
15
|
+
gem.add_dependency "dm-core", "~> 0.10.2"
|
16
|
+
gem.add_dependency "dm-more", "~> 0.10.2"
|
17
|
+
|
18
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
19
|
+
gem.add_development_dependency "rcov", ">= 0"
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'spec/rake/spectask'
|
28
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
34
|
+
spec.libs << 'lib' << 'spec'
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :spec => :check_dependencies
|
40
|
+
|
41
|
+
task :default => :spec
|
42
|
+
|
43
|
+
require 'rake/rdoctask'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "dm-groonga-adapter #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
52
|
+
|
data/VERSION.yml
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dm-groonga-adapter}
|
8
|
+
s.version = "0.1.0.pre"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["hiroyuki"]
|
12
|
+
s.date = %q{2010-04-08}
|
13
|
+
s.description = %q{datamapper adapter for groonga search engine}
|
14
|
+
s.email = %q{hello@hryk.info}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION.yml",
|
26
|
+
"dm-groonga-adapter.gemspec",
|
27
|
+
"examples/basic.rb",
|
28
|
+
"lib/groonga_adapter.rb",
|
29
|
+
"lib/groonga_adapter/adapter.rb",
|
30
|
+
"lib/groonga_adapter/local_index.rb",
|
31
|
+
"lib/groonga_adapter/model_ext.rb",
|
32
|
+
"lib/groonga_adapter/remote_index.rb",
|
33
|
+
"lib/groonga_adapter/remote_result.rb",
|
34
|
+
"lib/groonga_adapter/repository_ext.rb",
|
35
|
+
"lib/groonga_adapter/unicode_ext.rb",
|
36
|
+
"spec/rcov.opts",
|
37
|
+
"spec/shared/adapter_example.rb",
|
38
|
+
"spec/shared/search_example.rb",
|
39
|
+
"spec/spec.opts",
|
40
|
+
"spec/spec_helper.rb",
|
41
|
+
"spec/specs/adapter_spec.rb",
|
42
|
+
"spec/specs/remote_result_spec.rb",
|
43
|
+
"spec/specs/search_spec.rb"
|
44
|
+
]
|
45
|
+
s.homepage = %q{http://github.com/hryk/dm-groonga-adapter}
|
46
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
47
|
+
s.require_paths = ["lib"]
|
48
|
+
s.rubygems_version = %q{1.3.6}
|
49
|
+
s.summary = %q{datamapper adapter for groonga search engine}
|
50
|
+
s.test_files = [
|
51
|
+
"spec/shared/adapter_example.rb",
|
52
|
+
"spec/shared/search_example.rb",
|
53
|
+
"spec/spec_helper.rb",
|
54
|
+
"spec/specs/adapter_spec.rb",
|
55
|
+
"spec/specs/remote_result_spec.rb",
|
56
|
+
"spec/specs/search_spec.rb",
|
57
|
+
"examples/basic.rb"
|
58
|
+
]
|
59
|
+
|
60
|
+
if s.respond_to? :specification_version then
|
61
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
62
|
+
s.specification_version = 3
|
63
|
+
|
64
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
65
|
+
s.add_runtime_dependency(%q<groonga>, [">= 0.9"])
|
66
|
+
s.add_runtime_dependency(%q<dm-core>, ["~> 0.10.2"])
|
67
|
+
s.add_runtime_dependency(%q<dm-more>, ["~> 0.10.2"])
|
68
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
69
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
70
|
+
else
|
71
|
+
s.add_dependency(%q<groonga>, [">= 0.9"])
|
72
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
73
|
+
s.add_dependency(%q<dm-more>, ["~> 0.10.2"])
|
74
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
75
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
76
|
+
end
|
77
|
+
else
|
78
|
+
s.add_dependency(%q<groonga>, [">= 0.9"])
|
79
|
+
s.add_dependency(%q<dm-core>, ["~> 0.10.2"])
|
80
|
+
s.add_dependency(%q<dm-more>, ["~> 0.10.2"])
|
81
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
82
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
data/examples/basic.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
require 'dm-is-searchable'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
|
8
|
+
require 'groonga_adapter'
|
9
|
+
|
10
|
+
DataMapper.setup(:default, "sqlite3::memory:")
|
11
|
+
DataMapper.setup(:search, "groonga://#{Pathname(__FILE__).dirname.expand_path + "test/db"}")
|
12
|
+
|
13
|
+
class Image
|
14
|
+
include DataMapper::Resource
|
15
|
+
property :id, Serial
|
16
|
+
property :title, String
|
17
|
+
|
18
|
+
is :searchable # this defaults to :search repository, you could also do
|
19
|
+
# is :searchable, :repository => :ferret
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class Story
|
24
|
+
include DataMapper::Resource
|
25
|
+
property :id, Serial
|
26
|
+
property :title, String
|
27
|
+
property :author, String
|
28
|
+
|
29
|
+
# repository(:search) do
|
30
|
+
# # We only want to search on id and title.
|
31
|
+
# #properties(:search).clear
|
32
|
+
# property :id, Serial
|
33
|
+
# property :title, String
|
34
|
+
# end
|
35
|
+
|
36
|
+
is :searchable
|
37
|
+
end
|
38
|
+
|
39
|
+
Image.auto_migrate!
|
40
|
+
Story.auto_migrate!
|
41
|
+
|
42
|
+
image = Image.create(:title => "Oil Rig");
|
43
|
+
story = Story.create(:title => "Oil Rig", :author => "John Doe");
|
44
|
+
|
45
|
+
puts Image.search(:title => "Oil Rig").inspect # => [<Image title="Oil Rig">]
|
@@ -0,0 +1,207 @@
|
|
1
|
+
$KCODE = 'UTF-8'
|
2
|
+
module DataMapper
|
3
|
+
module Adapters
|
4
|
+
class GroongaAdapter < AbstractAdapter
|
5
|
+
|
6
|
+
def initialize(name, options)
|
7
|
+
super
|
8
|
+
Groonga::Context.default = nil # Reset Groonga::Context
|
9
|
+
@database = if @options[:port].nil? #unless File.extname(@options[:path]) == '.sock'
|
10
|
+
LocalIndex.new(@options)
|
11
|
+
else
|
12
|
+
RemoteIndex.new(@options)
|
13
|
+
end
|
14
|
+
@database.logger = ::DataMapper.logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def create(resources)
|
18
|
+
name = self.name
|
19
|
+
|
20
|
+
resources.each do |resource|
|
21
|
+
model = resource.model
|
22
|
+
attributes = resource.attributes(:field).to_mash
|
23
|
+
|
24
|
+
# Since we don't inspect the models before generating the indices,
|
25
|
+
# we'll map the resource's key to the :id column.
|
26
|
+
attributes[:id] ||= resource.key.first
|
27
|
+
|
28
|
+
unless @database.exist_table resource.model.name
|
29
|
+
@database.create_table(model.name,
|
30
|
+
model.properties(name),
|
31
|
+
model.key.first # <- key attribute.
|
32
|
+
)
|
33
|
+
end
|
34
|
+
@database.add model.name, attributes
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# This returns an array of Groonga docs (array of Groonga::Record) which can
|
39
|
+
# be used to instantiate objects by doc[:_type] and doc[:_id]
|
40
|
+
def read(query) # query is DataMapper::Query
|
41
|
+
table_name = query.model.name
|
42
|
+
grn_query = unless query.conditions.operands.empty?
|
43
|
+
create_grn_query(query)
|
44
|
+
else
|
45
|
+
""
|
46
|
+
end
|
47
|
+
grn_sort = create_grn_sort(query)
|
48
|
+
fields = query.fields
|
49
|
+
key = query.model.key(name).first
|
50
|
+
@database.search(table_name, grn_query, grn_sort).map do |lazy_doc|
|
51
|
+
fmap = fields.map { |p|
|
52
|
+
p_field = (p.field == "id") ? "_key" : p.field
|
53
|
+
[ p, p.typecast(lazy_doc[p_field]) ]
|
54
|
+
}.to_hash
|
55
|
+
fmap.update(
|
56
|
+
key.field => key.typecast(lazy_doc['_key'])
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def read_many(query)
|
62
|
+
read(query)
|
63
|
+
end
|
64
|
+
|
65
|
+
def read_one(query)
|
66
|
+
read(query).first
|
67
|
+
end
|
68
|
+
|
69
|
+
# TODO : implement #update
|
70
|
+
# def update(attributes, collection)
|
71
|
+
# query = collection.query
|
72
|
+
# 1
|
73
|
+
# end
|
74
|
+
|
75
|
+
def delete(collection)
|
76
|
+
query = collection.query
|
77
|
+
table_name = query.model.name
|
78
|
+
|
79
|
+
@database.delete(table_name, create_grn_query(query))
|
80
|
+
1
|
81
|
+
end
|
82
|
+
|
83
|
+
# This returns a hash of the resource constant and the ids returned for it
|
84
|
+
# from the search.
|
85
|
+
# { Story => ["1", "2"], Image => ["2"] }
|
86
|
+
# query is groonga query.
|
87
|
+
# options are;
|
88
|
+
# :operator
|
89
|
+
# :exact
|
90
|
+
# :longest_common_prefix
|
91
|
+
# :suffix
|
92
|
+
# :prefix
|
93
|
+
# :near
|
94
|
+
def search(model, groonga_query, groonga_sort=[], query_option={})
|
95
|
+
results = {}
|
96
|
+
groonga_sort = unless groonga_sort.empty?
|
97
|
+
groonga_sort
|
98
|
+
else
|
99
|
+
default_groonga_sort
|
100
|
+
end
|
101
|
+
@database.search(model.to_s, groonga_query, groonga_sort, query_option).each do |doc|
|
102
|
+
resources = results[Object.const_get(model.to_s)] ||= []
|
103
|
+
resources << doc[:_key]
|
104
|
+
end
|
105
|
+
results
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def default_groonga_sort
|
111
|
+
[[{:key => '_key', :order => :asc}], { :limit => -1, :offset => 0}]
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_grn_query(query)
|
115
|
+
conditions_statement(query.conditions)
|
116
|
+
end
|
117
|
+
|
118
|
+
## from dm-ferret-adapter ##
|
119
|
+
|
120
|
+
def conditions_statement(conditions)
|
121
|
+
case conditions
|
122
|
+
when Query::Conditions::NotOperation then negate_operation(conditions)
|
123
|
+
when Query::Conditions::AbstractOperation then operation_statement(conditions)
|
124
|
+
when Query::Conditions::AbstractComparison then comparison_statement(conditions)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def negate_operation(operation)
|
129
|
+
"- (#{conditions_statement(operation.operands.first)})"
|
130
|
+
end
|
131
|
+
|
132
|
+
def operation_statement(operation)
|
133
|
+
statements = []
|
134
|
+
|
135
|
+
operation.each do |operand|
|
136
|
+
statement = conditions_statement(operand)
|
137
|
+
|
138
|
+
if operand.respond_to?(:operands) && operand.operands.size > 1
|
139
|
+
statement = "(#{statement})"
|
140
|
+
end
|
141
|
+
|
142
|
+
statements << statement
|
143
|
+
end
|
144
|
+
|
145
|
+
join_with = operation.kind_of?(Query::Conditions::AndOperation) ? '+' : 'OR'
|
146
|
+
statements.join(" #{join_with} ")
|
147
|
+
end
|
148
|
+
|
149
|
+
def comparison_statement(comparison)
|
150
|
+
value = comparison.value
|
151
|
+
|
152
|
+
# TODO: move exclusive Range handling into another method, and
|
153
|
+
# update conditions_statement to use it
|
154
|
+
|
155
|
+
# break exclusive Range queries up into two comparisons ANDed together
|
156
|
+
if value.kind_of?(Range) && value.exclude_end?
|
157
|
+
operation = Query::Conditions::BooleanOperation.new(:and,
|
158
|
+
Query::Conditions::Comparison.new(:gte, comparison.subject, value.first),
|
159
|
+
Query::Conditions::Comparison.new(:lt, comparison.subject, value.last)
|
160
|
+
)
|
161
|
+
|
162
|
+
return "(#{operation_statement(operation)})"
|
163
|
+
end
|
164
|
+
|
165
|
+
operator = case comparison
|
166
|
+
when Query::Conditions::EqualToComparison then ''
|
167
|
+
when Query::Conditions::InclusionComparison then '@'
|
168
|
+
when Query::Conditions::RegexpComparison then raise NotImplementedError, 'no support for regexp match yet'
|
169
|
+
when Query::Conditions::LikeComparison then '@'
|
170
|
+
when Query::Conditions::GreaterThanComparison then '>'
|
171
|
+
when Query::Conditions::LessThanComparison then '<'
|
172
|
+
when Query::Conditions::GreaterThanOrEqualToComparison then '>='
|
173
|
+
when Query::Conditions::LessThanOrEqualToComparison then '<='
|
174
|
+
end
|
175
|
+
|
176
|
+
# We use property.field here, so that you can declare composite
|
177
|
+
# fields:
|
178
|
+
# property :content, String, :field => "title|description"
|
179
|
+
grn_field = (comparison.subject.field.to_s == 'id') ? '_key' : comparison.subject.field
|
180
|
+
[ "#{grn_field}:", ((value.is_a? String) ? quote_value(value) : value) ].join(operator)
|
181
|
+
end
|
182
|
+
|
183
|
+
## from dm-ferret-adapter ##
|
184
|
+
|
185
|
+
def create_grn_sort(query)
|
186
|
+
keys = []
|
187
|
+
options = { :limit => -1, :offset => 0}
|
188
|
+
options[:limit] = query.limit unless query.limit.nil?
|
189
|
+
options[:offset] = query.offset
|
190
|
+
if query.order.empty?
|
191
|
+
keys << {:key => '_key', :order => :asc}
|
192
|
+
else
|
193
|
+
query.order.each do |direction|
|
194
|
+
grn_field = (direction.target.name == :id) ? '_key' : direction.target.name
|
195
|
+
keys << { :key => grn_field.to_s, :order => direction.operator }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
[ keys, options ]
|
199
|
+
end
|
200
|
+
|
201
|
+
def quote_value(value)
|
202
|
+
return value.gsub(/"/, '\"').gsub(/\s/, '\ ')
|
203
|
+
end
|
204
|
+
|
205
|
+
end # DataMapper::Adapters::GroongaAdapter
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
$KCODE = "UTF-8"
|
2
|
+
module DataMapper
|
3
|
+
module Adapters
|
4
|
+
class GroongaAdapter::LocalIndex
|
5
|
+
attr_accessor :logger
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@options = options
|
9
|
+
@context = Groonga::Context.default
|
10
|
+
create_or_init_database
|
11
|
+
@tables = Mash.new
|
12
|
+
create_or_init_term_table
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(table_name, doc)
|
16
|
+
return unless exist_table(table_name)
|
17
|
+
table = table(table_name)
|
18
|
+
doc_id = doc.delete(:id)
|
19
|
+
record = table.add(doc_id)
|
20
|
+
|
21
|
+
doc.each do |k, v|
|
22
|
+
begin
|
23
|
+
if record.have_column? k
|
24
|
+
record[k] = v
|
25
|
+
else
|
26
|
+
puts "column #{k} is not defined."
|
27
|
+
end
|
28
|
+
rescue => e
|
29
|
+
puts record.inspect
|
30
|
+
puts record.columns.inspect
|
31
|
+
puts k
|
32
|
+
puts v
|
33
|
+
raise e
|
34
|
+
end
|
35
|
+
end
|
36
|
+
doc
|
37
|
+
end
|
38
|
+
|
39
|
+
# FIXME : WTF.
|
40
|
+
def delete(table_name, grn_query)
|
41
|
+
unless grn_query.empty?
|
42
|
+
# table = @tables[table_name]
|
43
|
+
ids = {}
|
44
|
+
# WTF start
|
45
|
+
@tables[table_name].select(grn_query, {}).records.each {|r|
|
46
|
+
# r.delete <-- Not work.
|
47
|
+
# ids[r[:dmid]] == true
|
48
|
+
ids[r['_key']] = true
|
49
|
+
}
|
50
|
+
@tables[table_name].records.each {|r|
|
51
|
+
# if ids[r[:dmid]] == true
|
52
|
+
if ids[r['_key']] == true
|
53
|
+
r.delete
|
54
|
+
end
|
55
|
+
}
|
56
|
+
# WTF end
|
57
|
+
#ids.each { |id| @tables[table_name].delete id }
|
58
|
+
end
|
59
|
+
1
|
60
|
+
end
|
61
|
+
|
62
|
+
# table_name : String
|
63
|
+
# grn_query : String (e.g., "title:@foovar"
|
64
|
+
# grn_sort : [{:key => "_id", :order => :asc }]
|
65
|
+
def search(table_name, grn_query, grn_sort=[], options={})
|
66
|
+
table = @tables[table_name]
|
67
|
+
table = @tables[table_name].select(grn_query, options) unless grn_query.empty?
|
68
|
+
|
69
|
+
if grn_sort.empty?
|
70
|
+
grn_sort << {:key => "_key", :order => :asc }
|
71
|
+
end
|
72
|
+
table.sort(*grn_sort)
|
73
|
+
end
|
74
|
+
|
75
|
+
def exist_table(table_name)
|
76
|
+
begin
|
77
|
+
Groonga::Hash.open(:name => table_name)
|
78
|
+
rescue Groonga::InvalidArgument
|
79
|
+
return false
|
80
|
+
rescue => e
|
81
|
+
raise e
|
82
|
+
else
|
83
|
+
return true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def open_table(table_name)
|
88
|
+
@tables[table_name] = Groonga::Hash.open(:name => table_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_table(table_name, properties, key_prop=nil)
|
92
|
+
key_type = (key_prop.nil?) ? Groonga::Type::UINT64 : trans_type(key_prop.type)
|
93
|
+
@tables[table_name] = Groonga::Hash.create(
|
94
|
+
:name => table_name,
|
95
|
+
:persistent => true,
|
96
|
+
:key_type => key_type
|
97
|
+
)
|
98
|
+
|
99
|
+
# add columns
|
100
|
+
properties.each do |prop|
|
101
|
+
type = trans_type(prop.type)
|
102
|
+
propname = prop.name.to_s
|
103
|
+
@tables[table_name].define_column(propname, type, {:persistent => true})
|
104
|
+
if type == "ShortText" || type == "Text" || type == "LongText"
|
105
|
+
index_column = add_term(table_name, propname)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def add_term(table, prop)
|
113
|
+
@tables['DMGterms'].define_index_column(
|
114
|
+
"#{table}_#{prop}", @tables[table],
|
115
|
+
:source => "#{table}.#{prop}"
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
# translate DataMapper::Property::TYPES to Groonga::Type
|
120
|
+
def trans_type(dmtype)
|
121
|
+
case dmtype.to_s
|
122
|
+
when 'String'
|
123
|
+
return Groonga::Type::SHORT_TEXT
|
124
|
+
when 'Text'
|
125
|
+
return Groonga::Type::TEXT
|
126
|
+
when 'Float'
|
127
|
+
return Groonga::Type::FLOAT
|
128
|
+
when 'Bool'
|
129
|
+
return Groonga::Type::BOOL
|
130
|
+
when 'Boolean'
|
131
|
+
return Groonga::Type::BOOLEAN
|
132
|
+
when 'Integer'
|
133
|
+
return Groonga::Type::INT32
|
134
|
+
when 'BigDecimal'
|
135
|
+
return Groonga::Type::INT64
|
136
|
+
when 'Time'
|
137
|
+
return Groonga::Type::TIME
|
138
|
+
when /^DataMapper::Types::(.+)$/
|
139
|
+
case $1
|
140
|
+
when "Boolean"
|
141
|
+
return Groonga::Type::BOOL
|
142
|
+
when "Serial"
|
143
|
+
return Groonga::Type::UINT32
|
144
|
+
end
|
145
|
+
else
|
146
|
+
return Groonga::Type::SHORT_TEXT
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def table(table_name)
|
151
|
+
unless @tables.key? table_name
|
152
|
+
if exist_table(table_name)
|
153
|
+
open_table(table_name)
|
154
|
+
else
|
155
|
+
false # no such table.
|
156
|
+
end
|
157
|
+
end
|
158
|
+
return @tables[table_name]
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_or_init_term_table
|
162
|
+
unless exist_table('DMGterms')
|
163
|
+
@tables['DMGterms'] = Groonga::Hash.create(:name => "DMGterms",
|
164
|
+
:persistent => true,
|
165
|
+
:key_type => Groonga::Type::UINT64,
|
166
|
+
:default_tokenizer => "TokenBigram")
|
167
|
+
else
|
168
|
+
open_table('DMGterms')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def create_or_init_database
|
173
|
+
# try to open database.
|
174
|
+
path = Pathname(@options[:path])
|
175
|
+
|
176
|
+
if path.exist? && path.file?
|
177
|
+
# open database
|
178
|
+
@database = Groonga::Database.open(path.to_s)
|
179
|
+
else
|
180
|
+
# check directory.
|
181
|
+
unless path.dirname.directory?
|
182
|
+
path.dirname.mkpath
|
183
|
+
end
|
184
|
+
# create database.
|
185
|
+
@database = Groonga::Database.create(:path => path.to_s)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Is
|
3
|
+
module Searchable
|
4
|
+
module ClassMethods
|
5
|
+
def fulltext_search(query, options={})
|
6
|
+
docs = repository(@search_repository).adapter.search(self.name, query, [], options)
|
7
|
+
self.all(options.merge(key.first => docs.values.flatten!))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|