dm-groonga-adapter 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,250 @@
1
+ require 'json'
2
+
3
+ module DataMapper
4
+ module Adapters
5
+ class GroongaAdapter::RemoteIndex
6
+ attr_accessor :logger
7
+ attr_accessor :context
8
+
9
+ def initialize(options)
10
+ @context = Groonga::Context.default
11
+ @context.connect(:host => options[:host], :port => options[:port])
12
+ # request "status" # <- TODO check connection with status command
13
+ end
14
+
15
+ def add(table_name, doc)
16
+ return unless exist_table(table_name)
17
+ doc_id = doc[:id] #doc.delete(:id)
18
+ record = []
19
+ record << doc.update("_key" => doc_id)
20
+ json = JSON.generate record
21
+ res = request "load --table #{table_name} --values #{Unicode.unescape(json.gsub(/"/, '\"').gsub(/\s/, '\ '))}"
22
+ result = GroongaResult::Count.new res
23
+ if result.success? && result.count > 0
24
+ return doc
25
+ else
26
+ throw "failed to load record. : #{result.err_code}"
27
+ end
28
+ end
29
+
30
+ def delete(table_name, grn_query)
31
+ self.search(table_name, grn_query).each do |i|
32
+ request "delete #{table_name} --id #{i['_id']}"
33
+ end
34
+ end
35
+
36
+ # select table [match_columns [query [filter [scorer [sortby [output_columns
37
+ # [offset [limit [drilldown [drilldown_sortby [drilldown_output_columns
38
+ # [drilldown_offset [drilldown_limit [output_type]]]]]]]]]]]]]]
39
+ def search(table_name, grn_query, grn_sort=[], options={})
40
+ sort_by, offset, limit = parse_grn_sort grn_sort
41
+ remote_query = (grn_query.empty?) ? "" : "--query #{grn_query}"
42
+ remote_sort_by = (sort_by.empty?) ? "" : "--sort-by #{sort_by}"
43
+ res = request "select #{table_name} #{remote_query} #{remote_sort_by} --offset #{offset} --limit #{limit}"
44
+ list = GroongaResult::List.new res
45
+ if list.success?
46
+ return list.to_a
47
+ else
48
+ throw list.err_msg
49
+ end
50
+ end
51
+
52
+ def exist_table(table_name)
53
+ res = request "table_list"
54
+ table_list = GroongaResult::List.new res
55
+ if table_list.success?
56
+ existence = false
57
+ table_list.each do |row|
58
+ existence = true if row[:name] == table_name
59
+ end
60
+ return existence
61
+ else
62
+ throw table_list.err_msg
63
+ end
64
+ end
65
+
66
+ def exist_column(table_name, column_name)
67
+ # groonga 1.4
68
+ # [["id","name","path","type","flags","domain"],[260,"title","test.0000104","var",49152,259]]
69
+ # groonga 1.7
70
+ # [
71
+ # [0,1269972586.4569,1.4e-05],
72
+ # [[["id", "UInt32"],["name","ShortText"],["path","ShortText"],["type","ShortText"],["flags","ShortText"],["domain", "ShortText"],["range", "ShortText"],["source","ShortText"]]]
73
+ # ]
74
+ res = request "column_list #{table_name}"
75
+ list = GroongaResult::List.new res
76
+ if list.success?
77
+ existence = false
78
+ list.each do |row|
79
+ existence = true if row[:name] == column_name
80
+ end
81
+ existence
82
+ else
83
+ throw list.err_msg
84
+ end
85
+ end
86
+
87
+ def create_table(table_name, properties, key_prop=nil)
88
+ key_type = (key_prop.nil?) ? "UInt64" : trans_type(key_prop.type)
89
+ # create table
90
+ res = request "table_create #{table_name} 0 #{key_type}";
91
+ result = GroongaResult::Base.new res
92
+ throw result.err_msg unless result.err_code == 0 || result.err_code == -22
93
+ properties.each do |prop|
94
+ type = trans_type(prop.type)
95
+ propname = prop.name.to_s
96
+ query = "column_create #{table_name} #{propname} 0 #{type}"
97
+ res = GroongaResult::Base.new(request query)
98
+ err = res.err_code
99
+
100
+ unless err == 0 || err == -22
101
+ throw "Create Column Failed : #{res.inspect} : #{query}"
102
+ end
103
+
104
+ if type == "ShortText" || type == "Text" || type == "LongText"
105
+ add_term(table_name, propname)
106
+ end
107
+ end
108
+ end
109
+
110
+ protected
111
+
112
+ def err_code(res)
113
+ return if res.nil?
114
+ code = res[0][0]
115
+ code
116
+ end
117
+
118
+ def create_term_table(table_name, key_prop="ShortText", tokenizer="TokenBigram")
119
+ res = request "table_create #{table_name} TABLE_PAT_KEY|KEY_NORMALIZE #{key_prop} Void #{tokenizer}"
120
+ throw "Fale to create term table." unless err_code(res) == 0 || err_code(res) == -22
121
+ true
122
+ end
123
+
124
+ def add_term(table_name, propname)
125
+ term_table_name = 'DMGTerms'
126
+ term_column_name = "#{table_name.downcase}_#{propname.downcase}"
127
+ # check existence of term table
128
+ unless exist_table term_table_name
129
+ create_term_table term_table_name
130
+ end
131
+ # check existence of column in term table
132
+ unless exist_column(term_table_name, term_column_name)
133
+ request "column_create DMGTerms #{term_column_name} COLUMN_INDEX|WITH_POSITION #{table_name} #{propname}"
134
+ end
135
+ end
136
+
137
+ def request(message)
138
+ @context.send message
139
+ self.logger.debug "Query: " + message
140
+ id, result = @context.receive
141
+ self.logger.debug "Result: " + result
142
+ if result == 'true'
143
+ true
144
+ elsif result == 'false'
145
+ false
146
+ else
147
+ JSON.parse(result)
148
+ end
149
+ end
150
+
151
+ def parse_grn_sort(grn_sort=[])
152
+ return "" if grn_sort == []
153
+ sort = grn_sort[0]
154
+ options = grn_sort[1]
155
+ sort_str = sort.map {|i|
156
+ desc = (i[:order] == :desc) ? '-' : ''
157
+ "#{desc}#{i[:key]}"
158
+ }.join(',')
159
+ [ sort_str, options[:offset], options[:limit] ]
160
+ end
161
+
162
+ def trans_type(dmtype)
163
+ case dmtype.to_s
164
+ when 'String'
165
+ return 'ShortText'
166
+ when 'Text'
167
+ return 'LongText'
168
+ when 'Float'
169
+ return 'Float'
170
+ when 'Bool'
171
+ return 'Bool'
172
+ when 'Boolean'
173
+ return 'Bool'
174
+ when 'Integer'
175
+ return 'Int32'
176
+ when 'BigDecimal'
177
+ return 'Int64'
178
+ when 'Time'
179
+ return 'Time'
180
+ when /^DataMapper::Types::(.+)$/
181
+ case $1
182
+ when "Boolean"
183
+ return 'Bool'
184
+ when "Serial"
185
+ return 'Int32'
186
+ end
187
+ else
188
+ return 'ShortText'
189
+ end
190
+ end
191
+ end # class GroongaAdapter::RemoteIndex
192
+ end # module Adapters
193
+ end # module DataMapper
194
+
195
+
196
+ __END__
197
+
198
+ def test_send
199
+ _context = Groonga::Context.new
200
+ _context.connect(:host => @host, :port => @port)
201
+ assert_equal(0, _context.send("status"))
202
+ id, result = _context.receive
203
+ assert_equal(0, id)
204
+ status, values = JSON.load(result)
205
+ return_code, start_time, elapsed, = status
206
+ assert_equal([0, ["alloc_count", "starttime", "uptime"]],
207
+ [return_code, values.keys.sort])
208
+ end
209
+
210
+ Commands
211
+
212
+ add
213
+ column_create
214
+ column_list
215
+ define_selector
216
+ delete
217
+ get
218
+ load
219
+ log_level
220
+ log_put
221
+ log_put
222
+ quit
223
+ select
224
+ set
225
+ shutdown
226
+ status
227
+ table_create
228
+ table_list
229
+ view_add
230
+
231
+ Types
232
+
233
+ Object 任意のテーブルに属する全てのレコード [1]
234
+ Bool bool型。trueとfalse。
235
+ Int8 8bit符号付き整数。
236
+ UInt8 8bit符号なし整数。
237
+ Int16 16bit符号付き整数。
238
+ UInt16 16bit符号なし整数。
239
+ Int32 32bit符号付き整数。
240
+ UInt32 32bit符号なし整数。
241
+ Int64 64bit符号付き整数。
242
+ UInt64 64bit符号なし整数。
243
+ Float ieee754形式の64bit浮動小数点数。
244
+ Time 1970年1月1日0時0分0秒からの経過マイクロ秒数を
245
+ 64bit符号付き整数で表現した値。
246
+ ShortText 4Kbyte以下の文字列。
247
+ Text 64Kbyte以下の文字列。
248
+ LongText 2Gbyte以下の文字列。
249
+ TokyoGeoPoint 日本測地系緯度経度座標。
250
+ WGS84GeoPoint 世界測地系緯度経度座標。
@@ -0,0 +1,101 @@
1
+ module DataMapper
2
+ module Adapters
3
+ module GroongaResult
4
+ class Base
5
+ attr_accessor :err_code
6
+ attr_accessor :err_msg
7
+ attr_accessor :start_time
8
+ attr_accessor :elapsed_time
9
+
10
+ def initialize(raw_result)
11
+ @err_code, @start_time, @elased_time, @err_msg = raw_result[0]
12
+ end
13
+
14
+ def success?
15
+ if @err_code == 0
16
+ true
17
+ else
18
+ false
19
+ end
20
+ end
21
+ end # class Result::Base
22
+
23
+ class Count < Base
24
+ attr_accessor :count
25
+ def initialize(raw_result)
26
+ super raw_result
27
+ # [[0,1270199923.22467,5.2e-05],1]
28
+ @count = raw_result[1]
29
+ end
30
+ end
31
+
32
+ class Status < Base
33
+ attr_accessor :alloc_count
34
+ attr_accessor :process_starttime
35
+ attr_accessor :uptime
36
+ attr_accessor :version
37
+
38
+ def initialize(raw_result)
39
+ super(raw_result)
40
+ if success?
41
+ @alloc_count = raw_result[1]['alloc_count']
42
+ @process_starttime = raw_result[1]['starttime']
43
+ @uptime = raw_result[1]['uptime']
44
+ @version = raw_result[1]['version']
45
+ end
46
+ end
47
+ end
48
+
49
+ class List < Base
50
+ include Enumerable
51
+ attr_accessor :columns
52
+ attr_accessor :rows
53
+ # attr_accessor :raw_rows
54
+ # attr_accessor :raw_columns
55
+ attr_accessor :size
56
+
57
+ def initialize(raw_result)
58
+ super(raw_result)
59
+ if success?
60
+ @raw_columns, @rows, @size = if raw_result[1].size > 1
61
+ # no count
62
+ raws = raw_result[1].dup
63
+ [raws.shift,raws,nil]
64
+ else
65
+ # with count
66
+ raws = raw_result[1].dup.shift
67
+ size = raws.shift.shift
68
+ rawcols = raws.shift
69
+ [rawcols, raws, size]
70
+ end
71
+ # columns
72
+ @columns = @raw_columns.map {|item| item[0] }
73
+ parse_rows
74
+ self
75
+ end
76
+ end
77
+
78
+ def each
79
+ @mash_rows.each do |m|
80
+ yield m
81
+ end
82
+ end
83
+
84
+ def to_a
85
+ return @mash_rows unless @mash_rows.nil?
86
+ []
87
+ end
88
+
89
+ def parse_rows
90
+ @mash_rows = @rows.map {|row|
91
+ m = Mash.new
92
+ @columns.each_with_index {|item, idx|
93
+ m[@columns[idx]] = row[idx]
94
+ }
95
+ m
96
+ }
97
+ end
98
+ end
99
+ end # now # module Result
100
+ end
101
+ end
@@ -0,0 +1,13 @@
1
+ module DataMapper
2
+ class Repository
3
+ # This accepts a ferret query string and an optional limit argument
4
+ # which defaults to all. This is the proper way to perform searches more
5
+ # complicated than DM's query syntax can handle (such as OR searches).
6
+ #
7
+ # See DataMapper::Adapters::GroongaAdapter#search for information on
8
+ # the return value.
9
+ def search(query, limit = :all)
10
+ adapter.search(query, limit)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ # http://d.hatena.ne.jp/cesar/20070401/p1
2
+ module Unicode
3
+ def escape(str)
4
+ ary = str.unpack("U*").map!{|i| "\\u#{i.to_s(16)}"}
5
+ ary.join
6
+ end
7
+
8
+ UNESCAPE_WORKER_ARRAY = []
9
+ def unescape(str)
10
+ str.gsub(/\\u([0-9a-f]{4})/) {
11
+ UNESCAPE_WORKER_ARRAY[0] = $1.hex
12
+ UNESCAPE_WORKER_ARRAY.pack("U")
13
+ }
14
+ end
15
+
16
+ module_function :escape, :unescape
17
+ end
@@ -0,0 +1,9 @@
1
+ require 'groonga'
2
+
3
+ require 'groonga_adapter/adapter'
4
+ require 'groonga_adapter/local_index'
5
+ require 'groonga_adapter/remote_index'
6
+ require 'groonga_adapter/remote_result'
7
+ require 'groonga_adapter/repository_ext'
8
+ require 'groonga_adapter/model_ext'
9
+ require 'groonga_adapter/unicode_ext'
data/spec/rcov.opts ADDED
@@ -0,0 +1,6 @@
1
+ --exclude "spec"
2
+ --sort coverage
3
+ --callsites
4
+ --xrefs
5
+ --profile
6
+ --text-summary
@@ -0,0 +1,53 @@
1
+ shared_examples_for "as adapter" do
2
+ before(:each) do
3
+ @adapter = DataMapper.setup(:default, "groonga://#{index_path}")
4
+
5
+ Object.send(:remove_const, :User) if defined?(User)
6
+ class ::User
7
+ include DataMapper::Resource
8
+
9
+ property :id, Serial
10
+ property :name, String
11
+ end
12
+
13
+ Object.send(:remove_const, :Photo) if defined?(Photo)
14
+ class ::Photo
15
+ include DataMapper::Resource
16
+
17
+ property :uuid, String, :default => proc { UUIDTools::UUID.random_create }, :key => true
18
+ property :happy, Boolean, :default => true
19
+ property :description, String
20
+ end
21
+
22
+ User.auto_migrate!
23
+ Photo.auto_migrate!
24
+ end
25
+
26
+ it 'should work with a model using id' do
27
+ u = User.create(:id => 2)
28
+ repository.search(User, '').should == { User => [ 2 ] }
29
+ end
30
+
31
+ it 'should work with a model using another key than id' do
32
+ p = Photo.create
33
+ repository.search(Photo, '').should == { Photo => [p.uuid] }
34
+ p.destroy!
35
+ end
36
+
37
+ it 'should allow lookups using Model#get' do
38
+ u = User.create(:id => 2, :name => "foovarbuz")
39
+ User.get(2).should == u
40
+ end
41
+
42
+ it 'should allow delete rows using Model#destroy' do
43
+ u = User.create(:id => 2, :name => "Alice")
44
+ u2 = User.create(:id => 3, :name => "Bob")
45
+ User.get(2).should == u
46
+ bob = User.get(3)
47
+ repository.search(User,'name:Bob').should == { User => [ 3 ] } #[User].size.should == 1
48
+ bob.destroy!.should == true
49
+ repository.search(User,'name:Bob').should == {}
50
+ repository.search(User,'name:Alice').should == {User => [ 2 ]}
51
+ end
52
+
53
+ end
@@ -0,0 +1,64 @@
1
+ shared_examples_for 'as is_search plugin' do
2
+
3
+ before(:each) do
4
+ DataMapper.setup(:default, "sqlite3::memory:")
5
+ DataMapper.setup(:search, "groonga://#{index_path}")
6
+ DataMapper::Logger.new($stderr, :debug)
7
+ Object.send(:remove_const, :Image) if defined?(Image)
8
+ class ::Image
9
+ include DataMapper::Resource
10
+ property :id, Serial
11
+ property :title, String
12
+
13
+ is :searchable # this defaults to :search repository, you could also do
14
+ end
15
+
16
+ Object.send(:remove_const, :Story) if defined?(Story)
17
+ class ::Story
18
+ include DataMapper::Resource
19
+ property :id, Serial
20
+ property :title, String
21
+ property :author, String
22
+
23
+ is :searchable
24
+ end
25
+
26
+ Story.auto_migrate!
27
+ Image.auto_migrate!
28
+ end
29
+
30
+ it 'should allow search with no operator' do
31
+
32
+ pending "grn expression may have bug." # FIXME
33
+
34
+ image = Image.create(:title => "Oil Rig");
35
+ story = Story.create(:title => "Oil Rig",
36
+ :author => "John Doe");
37
+ Image.search(:title => "Oil Rig").should == [image]
38
+ end
39
+
40
+ it 'should allow search with :like operator' do
41
+ image = Image.create(:title => "Oil Rig");
42
+ Image.search(:title.like => "Oil").should == [image]
43
+ image.title = "Owl Owl"
44
+ image.save
45
+ Image.search(:title.like => "Owl").should == [image]
46
+ end
47
+
48
+ it "should allow search with japanese" do
49
+ image = Image.create(:title => "お腹すいた");
50
+ Image.search(:title.like => "お腹").should == [image]
51
+ image.title = "すいてない"
52
+ image.save
53
+ Image.search(:title.like => "すいてない").should == [image]
54
+ end
55
+
56
+ it 'should allow search with all columns' do
57
+ story = Story.create(:title => "Oil Rig",
58
+ :author => "John Doe");
59
+ story2 = Story.create(:title => "Lolem ipsum",
60
+ :author => "John Doe");
61
+ # Story.fulltext_search("John").should == [story, story2] # <--- Crash on local index.
62
+ Story.fulltext_search("author:@John").should == [story, story2]
63
+ end
64
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'uuidtools'
3
+ require 'dm-core'
4
+ require 'dm-is-searchable'
5
+
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
+ SPEC_ROOT = Pathname(__FILE__).dirname.expand_path
9
+
10
+ require 'groonga_adapter'
11
+ require 'spec'
12
+ require 'spec/autorun'
13
+
14
+ (Pathname.new(__FILE__).parent + "shared").children.grep(/\.rb$/).each do |example|
15
+ puts example
16
+ require example
17
+ end
18
+
19
+ def load_driver(name, default_uri)
20
+ return false if ENV['ADAPTER'] != name.to_s
21
+
22
+ begin
23
+ DataMapper.setup(name, ENV["#{name.to_s.upcase}_SPEC_URI"] || default_uri)
24
+ DataMapper::Repository.adapters[:default] = DataMapper::Repository.adapters[name]
25
+ true
26
+ rescue LoadError => e
27
+ warn "Could not load do_#{name}: #{e}"
28
+ false
29
+ end
30
+ end
31
+
32
+ ENV['ADAPTER'] ||= 'sqlite3'
33
+
34
+ HAS_SQLITE3 = load_driver(:sqlite3, 'sqlite3::memory:')
35
+ HAS_MYSQL = load_driver(:mysql, 'mysql://localhost/dm_core_test')
36
+ HAS_POSTGRES = load_driver(:postgres, 'postgres://postgres@localhost/dm_core_test')
37
+
38
+ def local_groonga_path
39
+ Pathname(SPEC_ROOT) + 'test/index'
40
+ end
41
+ def remote_groonga_path
42
+ ENV["DM_GRN_URL"] || "127.0.0.1:10041" # "192.168.81.132:8888" <- 1.4.0
43
+ end
44
+
45
+ # Spec::Runner.configure do |config|
46
+ # end
@@ -0,0 +1,25 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "local_index adapter" do
4
+ def index_path;local_groonga_path;end
5
+
6
+ after(:all) do
7
+ Pathname.new(index_path).parent.children.each do |f|
8
+ f.delete
9
+ end
10
+ end
11
+
12
+ before(:each) do
13
+ # remove indeces before running spec.
14
+ Pathname.new(index_path).parent.children.each do |f|
15
+ f.delete
16
+ end
17
+ end
18
+
19
+ it_should_behave_like "as adapter"
20
+ end
21
+
22
+ describe "remote_index adapter" do
23
+ def index_path;remote_groonga_path;end
24
+ it_should_behave_like "as adapter"
25
+ end