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