dm-groonga-adapter 0.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|