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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
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,5 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 0
4
+ :build: pre
5
+ :major: 0
@@ -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