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