drip 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +0 -2
- data/drip.gemspec +1 -1
- data/drip.txt +1068 -190
- data/lib/drip.rb +146 -9
- data/lib/drip/version.rb +1 -1
- data/lib/my_drip.rb +4 -0
- data/sample/copocopo.rb +1 -1
- data/sample/demo4book/crawl.rb +56 -0
- data/sample/demo4book/demo_ui.rb +71 -0
- data/sample/demo4book/demo_ui_webrick.rb +69 -0
- data/sample/demo4book/index.rb +96 -0
- data/sample/demo4book/query2.rb +47 -0
- data/sample/demo4book/query2_test.rb +18 -0
- data/test/basic.rb +131 -0
- metadata +21 -5
data/Gemfile
CHANGED
data/drip.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.description = ""
|
13
13
|
|
14
14
|
s.rubyforge_project = "drip"
|
15
|
-
|
15
|
+
s.add_dependency "rbtree"
|
16
16
|
s.files = `git ls-files`.split("\n")
|
17
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
data/drip.txt
CHANGED
@@ -1,112 +1,70 @@
|
|
1
1
|
*ストリーム指向のストレージDrip
|
2
2
|
|
3
|
-
|
3
|
+
この章では筆者が最近夢中になっているストリーム指向のストレージ、Dripについて紹介します。Dripはストレージであると同時に、プロセス間を協調のメカニズムでもあります。このくだりを聴いてもRindaとの共通部分が多くあると感じるでしょう。実際にRindaのアプリケーションを書いた経験を元にして書かれました。DripはRindaを置き換えるものではありません。どちらかというとオブジェクトの貯蔵庫であって、オブジェクト指向データベース、Key Value Storeやマルチディメンジョンのリストなど一連のストレージの習作を出発点としました。
|
4
4
|
|
5
|
-
この節ではRinda::TupleSpaceをおさらいしながら、TupleSpaceに永続化したPTupleSpaceの概要を紹介し、その制約について考えます。
|
6
|
-
PTupleSpaceはTupleSpaceのサブクラスです。タプルの状態の変化を逐次二次記憶にログして、次回の起動に備えます。PTupleSpaceを再起動すると最後の(最新の)タプルの状態のままに復元されます。
|
7
5
|
|
8
|
-
|
6
|
+
**Dripとはなにか
|
9
7
|
|
10
|
-
|
8
|
+
Dripは追記型のストレージの一種で、Rubyオブジェクトを時系列にログします。Dripにはオブジェクトの追記のみ可能で、削除や更新はできません。dRubyのRMIを考慮した、局所的で安価なブラウズ用APIを用意してあります。オブジェクトのまとめ転送や、簡単なパターンによるフィルタ、シークをなど、です。
|
9
|
+
また、Dripはプロセス間の同期メカニズムでもあります。新しいオブジェクトの到着を待合せることができます。Dripでは一度保存されたオブジェクトは変化することはありません。複数のプロセスがばらばらの時刻に読み出した情報はどれも同じものですし、誰かが読んだオブジェクトを別の誰かが変更することはありません。この特性は分散ファイルシステムでよく見られる特性で、情報を排他的にアクセスしなくてはならない状況を減らすことができます。
|
11
10
|
|
12
|
-
|
11
|
+
Dripはちょっとしたオブジェクトの保存先であり、プロセス間通信、バッチ処理のエーテルであり、ライフログです。単純な仕組みであるため、さまざまな用途への応用が考えられますし、それ故に使い途を想像するのが難しいとも言えます。私のDripを次のようなアプリケーションに使いました。
|
13
12
|
|
14
|
-
|
13
|
+
- バッチ処理のミドルウェア
|
14
|
+
- Wikiシステムのストレージと全文検索
|
15
|
+
- Twitterのタイムラインのアーカイブとbotフレームワーク
|
16
|
+
- irbでの作業中のメモ
|
15
17
|
|
16
|
-
|
18
|
+
ちょっと雲をつかむような感じですね。次の節から、身近な同期メカニズムであるQueue、身近なオブジェクトの貯蔵庫であるHashとの違いをそれぞれ見ながら、Dripを紹介します。
|
17
19
|
|
18
|
-
|
20
|
+
**Queueとの比較
|
19
21
|
|
20
|
-
|
21
|
-
def succ
|
22
|
-
_, count = @ts.take([:count, nil])
|
23
|
-
count += 1
|
24
|
-
yield(count)
|
25
|
-
ensure
|
26
|
-
@ts.write([:count, count) if count
|
27
|
-
end
|
28
|
-
||<
|
29
|
-
|
30
|
-
これは[:count, 整数]のタプルを取り出し、一つ大きくしてまた書き込むスクリプトです。伝票を取り出し、カウンタを一つ進め、最後にTupleSpaceに書き戻します。伝票がプロセスにある間は、別のプロセスは伝票をTupleSpaceから読んだり、取り出したりすることはできないので安全にカウンタを操作できます。さて、もしも伝票がプロセスにある間にそのプロセスがクラッシュしたらどうなるでしょう。PTupleSpaceは自身の中にある伝票しか復元できませんから、その伝票は失われたままです。このカウンタを操作するプロセス群は全て停まってしまいます。こういった使い方(協調に使うケースの多くはそうなんだと思うのですが)をする場合、TupleSpaceだけでなく関係するプロセス群も再起動する必要があるだけでなく、TupleSpace内のタプルも初期状態にする必要があります。せっかくタプルの状態を復元できるようにしたというのに‥。
|
22
|
+
まずQueueと比較しながらDripにおけるプロセス間の協調の違いを見てましょう。
|
31
23
|
|
32
|
-
|
24
|
+
ここでのQueueとはRubyに付属のQueueクラスです。QueueはFIFOのバッファで、要素は任意のオブジェクトです。Queueにオブジェクトを追加するのはpush、オブジェクトを取り出すのはpopです。popはオブジェクトを返すと同時に、Queueの中からそのオブジェクトを削除します。
|
25
|
+
同時に複数のスレッドからpopすることも可能ですが、一つの要素は一つのスレッドにだけ届きます。同じ要素が複数のpopに届くことはありません。
|
26
|
+
空のQueueに対してpopを行うとpopはブロックします。新しいオブジェクトが追加され、そしてそのオブジェクトを獲得したただ一人のスレッドに対してオブジェクトを届けます。
|
33
27
|
|
34
|
-
|
28
|
+
Dripにおいてpopに相当する操作はreadです。readは指定したカーソルより新しい要素を返します。ただし、Dripの中から要素を削除することはありません。複数のスレッドが同じカーソルでreadした場合には、それぞれのスレッドに同じ要素を返します。
|
29
|
+
カーソルよりも新しい要素がない場合、readはブロックします。新しいオブジェクトがwriteされるとreadのブロックはとけて、新しい要素を返します。この場合も、複数のスレッドに同じ要素が届きます。
|
35
30
|
|
36
|
-
|
31
|
+
DripがQueueやRindaとよく似ているポイントは、要素の到着を待つことができるところです。
|
37
32
|
|
33
|
+
また異なるポイントは要素を消費するかどうかです。Queueのpopは要素を消費しますが、Dripのreadでは要素は減りません。これは何度でも/何人でも読めるということです。Rindaではアプリケーションのバグやクラッシュによるタプルの紛失はシステム全体のダウンを意味することがありますが、Dripでは要素の紛失を気にする必要はありません。
|
38
34
|
|
39
|
-
|
35
|
+
具体的なコードでDripのreadの様子を見ていきましょう。
|
40
36
|
|
41
|
-
|
42
|
-
TupleSpaceはタプル群を扱う集合構造です。同じ情報を複数持つことができるので、Bagと言えるでしょう。
|
43
|
-
最近の流行言葉にKVSという言葉ありますね。キーと値で表現するなら、同じキーを持つ要素の重複を許すストレージです。キーしかなくて、キーが値、にも見えますが。
|
37
|
+
***ここで使用するメソッド
|
44
38
|
|
45
|
-
|
46
|
-
|
47
|
-
TupleSpaceで辞書を模倣するのはやっかいです。[キー, 値]というタプルで辞書を構成仕様とした場合を考えてみましょう。まずデータを読むのは次のように書けそうです。
|
48
|
-
>|ruby|
|
49
|
-
@ts.read([キー, nil])
|
50
|
-
||<
|
51
|
-
では要素の追加はどうでしょう。
|
52
|
-
>|ruby|
|
53
|
-
@ts.write([キー, 値])
|
54
|
-
||<
|
55
|
-
このような単純なwriteでは重複を防ぐことはできません。全体をロックして、そのキーのタプルを削除してからwriteする必要があります。
|
56
|
-
>|ruby|
|
57
|
-
def []=(key, value)
|
58
|
-
lock = @ts.take([:global_lock])
|
59
|
-
@ts.take([key, nil], 0) rescue nil
|
60
|
-
@ts.write([key, value])
|
61
|
-
ensure
|
62
|
-
@ts.write(lock) if lock
|
63
|
-
end
|
64
|
-
||<
|
65
|
-
このグローバルなロックは実はデータを読むときにも必要です。なぜなら、そのキーの情報を別のスレッドが更新中かもしれないからです。
|
39
|
+
ここで使用するメソッドは主に二つです。
|
66
40
|
|
67
41
|
>|ruby|
|
68
|
-
|
69
|
-
lock = @ts.take([:global_lock])
|
70
|
-
_, value = @ts.read([key, nil], 0) rescue nil
|
71
|
-
return value
|
72
|
-
ensure
|
73
|
-
@ts.write(lock) if lock
|
74
|
-
end
|
42
|
+
Drip#write(obj, *tags)
|
75
43
|
||<
|
76
44
|
|
77
|
-
|
45
|
+
writeメソッドはDripの状態を変化させる唯一の操作で、要素を追加します。要素objをDripに格納し、格納されたキーを返します。objへのアクセスを容易にするために、複数のタグをしていできます。タグの使い方はあとで説明します。
|
78
46
|
|
79
|
-
|
47
|
+
もう一つのメソッドはreadです。
|
80
48
|
|
81
49
|
>|ruby|
|
82
|
-
|
83
|
-
lock = @ts.take([:global_lock])
|
84
|
-
@ts.read_all([nil, nil]).each(&blk)
|
85
|
-
ensure
|
86
|
-
@ts.write(lock) if lock
|
87
|
-
end
|
50
|
+
Drip#read(key, n=1, at_least=1, timeout=nil)
|
88
51
|
||<
|
89
52
|
|
90
|
-
|
91
|
-
|
53
|
+
Dripをブラウズする基本となるメソッドがreadです。keyは注目点(カーソル)で、keyよりも後に追加された要素のキーと値の組をn個の配列で返します。要素がat_least個そろうまで、readはブロックします。timeoutを指定することができます。
|
54
|
+
説明が長いですね。要するに「新しい要素をn返せ。at_least個揃うまでは待機せよ。」です。
|
92
55
|
|
93
|
-
流行のストレージには、常にキーでソートされているシーケンスを持つものがあります。並んでいることを利用して、大きな空間をブラウズするのが得意です。キーを工夫することでバージョン付きの情報を蓄えることもできます。RindaのTupleSpaceには、タプルを順序付けて並べることはできませんから、これを低コストで模倣するのは難しいです。
|
94
56
|
|
95
|
-
|
57
|
+
***Dripのインストールと起動
|
96
58
|
|
97
|
-
|
59
|
+
おっと。Dripのインストールを忘れていました。DripはRBTreeという赤黒木の外部ライブラリを使用します。gemを用意していただいたので次のようにインストールして下さい。
|
98
60
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
***Dripをインストールする
|
104
|
-
|
105
|
-
(井上さんgems化してくださーい。)
|
61
|
+
>||
|
62
|
+
% gem ?????
|
63
|
+
||<
|
106
64
|
|
107
|
-
|
65
|
+
次にDripサーバを起動します。
|
108
66
|
|
109
|
-
Drip
|
67
|
+
Dripはデフォルトでは二次記憶としてプレーンなファイルを使います。Dripを生成するにはファイルを置くディレクトリを指定します。次のスクリプト(drip_s.rb)はDripを生成しdRubyでサービスするものです。
|
110
68
|
|
111
69
|
>|ruby|
|
112
70
|
require 'drip'
|
@@ -129,150 +87,469 @@ DRb.thread.join
|
|
129
87
|
|
130
88
|
Dripにquitメソッドを追加しています。これはRMI経由でこのプロセスを終了させるもので、Dripが二次記憶への操作をしていないとき(synchronize中)を待ってから終わらせます。
|
131
89
|
|
132
|
-
|
133
|
-
|
134
|
-
ターミナル1
|
90
|
+
次のように起動できます。
|
135
91
|
>||
|
136
92
|
% ruby drip_s.rb
|
137
93
|
||<
|
138
94
|
|
139
|
-
|
95
|
+
***MyDrip
|
140
96
|
|
141
|
-
Drip
|
97
|
+
MacOSXなどPOSIXなOS専用ですが、MyDripという1人用の起動が簡単なDripサーバも用意されています。これは、ホームディレクトリの直下に.dripというディレクトリを作成し、この中をストレージとするDripで、UNIXドメインソケットを使ってサービスします。UNIXドメインソケットですから、ファイルの権限、可視性によって利用者を制限できます。また、UNIXドメインソケットのファイル名はホームディレクトリ以下のいつも決まったパスで接続できます。
|
98
|
+
TCPの場合、固定にするにはそのマシンの中である番号のポートをあるサービスに割り当てる、とみんなで約束を守る必要があり、dRubyのURIを固定にするのに面倒なところがあります。それに対して、各ユーザのホームディレクトリの下のファイルを使う場合にはみんなで約束しあう必要がありませんから、URIを機械的に決めるのが簡単です。
|
99
|
+
|
100
|
+
MyDripを利用するにはmy_dripをrequireします。
|
101
|
+
起動してみましょう。
|
142
102
|
|
143
|
-
ターミナル2
|
144
103
|
>||
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
104
|
+
ターミナル1
|
105
|
+
% irb -r my_drip --simple-prompt
|
106
|
+
>> MyDrip.invoke
|
107
|
+
=> 51252
|
108
|
+
>> MyDrip.class
|
109
|
+
=> DRb::DRbObject
|
150
110
|
||<
|
151
111
|
|
152
|
-
|
153
|
-
キーがおおよその時刻に変換できるのは、人間があとでデータを調べるときに多少は便利です。Dripには同時にただ一つのオブジェクトだけが書き込めます。さまざまな事象は同時にいくつも発生しますが、Dripがそれを観測するのは同時にはただ一つです。事象が発生した時刻ではなく、Dripがそれを知った時刻と考えて下さい。
|
112
|
+
MyDripはこの固定のポートを指すDRbObjectですが、特別にinvokeメソッドが定義されています。MyDrip.invokeは新しいプロセスをforkし、必要であればDripデーモン起動します。すでに自分用のMyDripが動いている場合にはなにもせずに終了します。なお、MyDripを終了させるにはMyDrip.quitメソッドを使います。
|
154
113
|
|
155
|
-
|
114
|
+
MyDripはirb実行中にちょっとしたオブジェクトのメモをとるのにも使える便利なデーモンです。筆者の環境ではいつもMyDripを起動してあり、Twitterのタイムラインを常にアーカイブしたり、メモをしたりbotのミドルウェアになったりしています。
|
115
|
+
|
116
|
+
私の.irbrcは次のようにmy_dripをrequireしています。irbを使っているときはいつでもMyDripにメモできます。
|
117
|
+
|
118
|
+
>|ruby|
|
119
|
+
require 'my_drip'
|
120
|
+
||<
|
121
|
+
|
122
|
+
|
123
|
+
以降の実験では、主にMyDripを利用します。MyDripが利用できない環境の方は、次のように定義した"my_drip.rb"を用意することでdrip_s.rbのサービスを代用して使えます。
|
124
|
+
|
125
|
+
>|ruby|
|
126
|
+
MyDrip = DRbObject.new_with_uri('druby://localhost:54321')
|
127
|
+
||<
|
128
|
+
|
129
|
+
***再びQueueとの比較
|
130
|
+
|
131
|
+
MyDripデーモン(あるいは代用となるdrip_s.rb)が起動している状態で実験です。
|
132
|
+
|
133
|
+
writeメソッドを使ってオブジェクトを二つ追加します。writeはDripを変化させる唯一のメソッドです。writeメソッドの戻り値は追加された要素と関連付けられたキーです。キーは時刻(usec)から作られた正の整数で、64bitマシンではしばらくの間はFixnumとなります。
|
134
|
+
|
135
|
+
>||
|
136
|
+
ターミナル2
|
137
|
+
% irb -r my_drip --simple-prompt
|
138
|
+
>> MyDrip.write('Hello')
|
139
|
+
=> 1312541947966187
|
140
|
+
>> MyDrip.write('world')
|
141
|
+
=> 1312541977245158
|
142
|
+
||<
|
143
|
+
|
144
|
+
つぎにDripからデータを読んでみます。
|
156
145
|
|
157
146
|
>||
|
158
|
-
|
159
|
-
|
160
|
-
>>
|
161
|
-
=>
|
147
|
+
ターミナル3
|
148
|
+
% irb -r my_drip --simple-prompt
|
149
|
+
>> MyDrip.read(0, 1)
|
150
|
+
=> [[1312541947966187, "Hello"]]
|
162
151
|
||<
|
163
152
|
|
164
|
-
|
153
|
+
readはカーソルからn個の要素を読むメソッドで、キーと値のペアの配列を返します。
|
154
|
+
順に読むには次のようにカーソルを動かしながらreadすると良いでしょう。
|
165
155
|
|
166
|
-
|
156
|
+
>||
|
157
|
+
>> k = 0
|
158
|
+
=> 0
|
159
|
+
>> k, v = MyDrip.read(k, 1)[0]
|
160
|
+
=> [1312541947966187, "Hello"]
|
161
|
+
>> k, v = MyDrip.read(k, 1)[0]
|
162
|
+
=> [1312541977245158, "World"]
|
163
|
+
||<
|
167
164
|
|
168
|
-
|
165
|
+
二つ読めました。さらに読むとどうなるでしょう。
|
169
166
|
|
170
|
-
|
171
|
-
|
167
|
+
>||
|
168
|
+
>> k, v = MyDrip.read(k, 1)[0]
|
172
169
|
||<
|
173
170
|
|
174
|
-
|
171
|
+
kよりも新しい要素がないのでブロックします。ターミナル2から新しい要素を追加するとブロックがとけ、そのオブジェクトが読めるはずです。
|
175
172
|
|
176
|
-
|
173
|
+
>||
|
174
|
+
ターミナル2
|
175
|
+
>> MyDrip.write('Hello, Again')
|
176
|
+
=> 1312542657718320
|
177
|
+
||<
|
177
178
|
|
178
179
|
>||
|
179
|
-
>>
|
180
|
-
=> [
|
180
|
+
>> k, v = MyDrip.read(k, 1)[0]
|
181
|
+
=> [1312542657718320, "Hello, Again"]
|
181
182
|
||<
|
182
183
|
|
183
|
-
|
184
|
+
どうですか?待合せできていますか?
|
184
185
|
|
185
|
-
|
186
|
+
読み手を増やしてまた0から読んでみましょう。
|
186
187
|
|
187
188
|
>||
|
189
|
+
ターミナル4
|
190
|
+
% irb -r my_drip --simple-prompt
|
188
191
|
>> k = 0
|
189
192
|
=> 0
|
190
|
-
>> k, v
|
191
|
-
=> [
|
192
|
-
>> k, v
|
193
|
-
=> [
|
194
|
-
>> k, v
|
195
|
-
=> [
|
196
|
-
>> k, v, *tag = drip.read(k)[0]
|
193
|
+
>> k, v = MyDrip.read(k, 1)[0]
|
194
|
+
=> [1312541947966187, "Hello"]
|
195
|
+
>> k, v = MyDrip.read(k, 1)[0]
|
196
|
+
=> [1312541977245158, "World"]
|
197
|
+
>> k, v = MyDrip.read(k, 1)[0]
|
198
|
+
=> [1312542657718320, "Hello, Again"]
|
197
199
|
||<
|
198
200
|
|
199
|
-
|
201
|
+
同じ要素が読めました。DripではQueueとちがって要素を消費しませんから、同じ情報をなんども読めます。その代わりにどの辺りの要素を読むのか、readのたびに指定しなくてはなりません。
|
200
202
|
|
201
|
-
|
203
|
+
ここでMyDripを再起動させましょう。quitメソッドを呼ぶとだれもwriteしていないときを見計らってプロセスを終了させます。再起動するにはinvokeを呼びます。MyDrip.invokeはログが大きいと時間がかかるときがあります。
|
204
|
+
|
205
|
+
>||
|
206
|
+
ターミナル1
|
207
|
+
>> MyDrip.quit
|
208
|
+
=> #<Thread:...>
|
209
|
+
>> MyDrip.invoke
|
210
|
+
=> 61470
|
211
|
+
||<
|
212
|
+
|
213
|
+
readメソッドで先ほどの状態になっているか確認してみましょう。
|
202
214
|
|
203
|
-
ターミナル3
|
204
215
|
>||
|
205
|
-
|
206
|
-
>>
|
207
|
-
=>
|
208
|
-
|
209
|
-
|
216
|
+
ターミナル1
|
217
|
+
>> MyDrip.read(0, 3)
|
218
|
+
=> [[1312541947966187, "Hello"], [1312541977245158, "World"], [1312542657718320, "Hello, Again"]]
|
219
|
+
||<
|
220
|
+
|
221
|
+
***実験のまとめ
|
222
|
+
Queueと似ている点は、時系列に並んだデータを順に取り出せるところ、データの到着を待合せできるところです。Queueと異なる点はデータが減らないところです。同じ要素を複数のプロセスから読めますし、同じプロセスが何度もよむこともできます。経験上、バッチ処理は開発中も運用中も何度も停まりますよね。Dripでは工夫すれば先ほどの状態から処理を再開できます。途中からでも最初からでもやり直すチャンスがあります。
|
223
|
+
またQueueとの比較を通じて基本となる二つの操作、write、readを紹介しました。
|
224
|
+
|
225
|
+
**Hashとの比較
|
226
|
+
|
227
|
+
ここではKVS、あるいはHashとDripを比較し、それを通じてDripの操作を学びます。
|
228
|
+
RubyのHashはキーと値が組になった連想配列で、連想配列の実装にハッシュ表を使うことからHashと呼ばれています。あるキーと関連するのは一つの値です。Dripではwrite時に指定できるタグを使ってHashを模倣することができます。
|
229
|
+
|
230
|
+
***タグ
|
231
|
+
|
232
|
+
Drip#writeには格納したいオブジェクトのほかにタグを指定することができます。タグはStringです。一つのオブジェクトに複数のタグをつけることができます。あるタグを指定してreadすることができるため、オブジェクトをあとで取り出すのが容易になります。このタグを利用するとHashを模倣することができます。
|
233
|
+
|
234
|
+
タグをHashのキーと考えてみましょう。Dripにおいて「タグをつけてwriteする」のはHashにおいては「キーに関連する値を設定する」ことになります。「タグをもつ最新の値をreadする」のはHashではキーに関連す値を取り出すことと同じです。「最新の値」を取り出せばHashと同様ですが、それ以前の値を取り出すことができますから、この方法で模倣したHashは、変更履歴を持つHashと言えます。
|
235
|
+
|
236
|
+
***ここで使用するAPI
|
237
|
+
|
238
|
+
ここで新たに使用するAPIはheadとread_tagです。
|
239
|
+
|
240
|
+
>|ruby|
|
241
|
+
Drip#head(n=1, tag=nil)
|
242
|
+
||<
|
243
|
+
|
244
|
+
headは先頭からn個の要素の配列を返します。tagを指定すると、そのtagを持つ要素だけを選んでn個返します。Drip中の要素数がnより小さくてもHeadはブロックしません。先頭のn個を覗くだけです。
|
245
|
+
|
246
|
+
>|ruby|
|
247
|
+
Drip#read_tag(key, tag, n=1, at_least=1, timeout=nil)
|
248
|
+
||<
|
249
|
+
|
250
|
+
read_tagの基本的な動作はreadと同じですが、tagを指定するところが違います。tagをもつ要素だけをreadします。readと同じですから、keyより新しい要素の数がat_least個に満たない場合は、新しいデータが追加されるまでブロックします。あるタグを持つ要素の追加を待ち合わせることができるわけです。
|
251
|
+
|
252
|
+
***実験
|
253
|
+
|
254
|
+
タグとhead、read_tagを組み合わせてHashを模倣してみましょう。先ほどのMyDripをそのまま使います。
|
255
|
+
|
256
|
+
まず値の設定です。
|
257
|
+
|
258
|
+
>|ruby|
|
259
|
+
hash['seki.age'] = 29
|
210
260
|
||<
|
211
261
|
|
262
|
+
上記のhashへの操作に相当するのは次の通りです。'seki.age'というタグをつけて29をwriteします。
|
263
|
+
|
264
|
+
|
265
|
+
>||
|
212
266
|
ターミナル2
|
267
|
+
>> MyDrip.write(29, 'seki.age')
|
268
|
+
=> 1313358208178481
|
269
|
+
||<
|
270
|
+
|
271
|
+
値の取り出しにはheadが良いでしょう。'seki.age'タグを持つ要素を先頭から一つ要求します。
|
272
|
+
|
213
273
|
>||
|
214
|
-
|
274
|
+
ターミナル2
|
275
|
+
>> MyDrip.head(1, 'seki.age')
|
276
|
+
=> [[1313358208178481, 29, "seki.age"]]
|
215
277
|
||<
|
216
278
|
|
217
|
-
|
279
|
+
一つの要素は[キー, 値, 任意個のタグ]で、これらの配列が返ります。値だけを見たいのであれば次のようにしても良いでしょう。
|
280
|
+
|
281
|
+
>||
|
282
|
+
ターミナル2
|
283
|
+
>> k, v = MyDrip.head(1, 'seki.age')
|
284
|
+
=> [[1313358208178481, 29, "seki.age"]]
|
285
|
+
>> v
|
286
|
+
=> 29
|
287
|
+
||<
|
288
|
+
|
289
|
+
今度は値を再設定してみます。
|
290
|
+
|
291
|
+
>|ruby|
|
292
|
+
hash['seki.age'] = 49
|
293
|
+
||<
|
218
294
|
|
219
|
-
|
295
|
+
Hashでいうと上記のような操作です。'seki.age'に関連する値を49と変更するには、先ほどと同様に'seki.age'というタグをつけて49をwriteすればよいです。writeして、headで確認してみましょう。
|
220
296
|
|
297
|
+
>||
|
221
298
|
ターミナル2
|
299
|
+
>> MyDrip.write(49, 'seki.age')
|
300
|
+
=> 1313358584380683
|
301
|
+
>> MyDrip.head(1, 'seki.age')
|
302
|
+
=> [[1313358584380683, 49, "seki.age"]]
|
303
|
+
||<
|
304
|
+
|
305
|
+
変更履歴は過去のデータを取り出せばわかります。headを使って最新10バージョンの履歴を調べます。
|
306
|
+
|
222
307
|
>||
|
223
|
-
|
308
|
+
ターミナル2
|
309
|
+
>> MyDrip.head(10, 'seki.age')
|
310
|
+
=> [[1313358208178481, 29, "seki.age"], [1313358584380683, 49, "seki.age"]]
|
224
311
|
||<
|
225
312
|
|
226
|
-
|
313
|
+
先頭から10個の要素を要求しましたが、いまDripの中にある'seki.age'を持つ要素は二つだけなので、2要素のArrayが返りました。結果が複数返る場合、配列は旧い方から新しい方へ向けて並んでいます。
|
314
|
+
|
315
|
+
では存在しないキー(Hashでいうところのキー)を問い合わせるとどうなるでしょう。
|
227
316
|
|
228
|
-
ターミナル1
|
229
317
|
>||
|
230
|
-
|
231
|
-
|
232
|
-
|
318
|
+
ターミナル2
|
319
|
+
>> MyDrip.head(1, 'sora_h.age')
|
320
|
+
=> []
|
321
|
+
||<
|
322
|
+
|
323
|
+
空の配列が返りました。ブロックもしません。headはブロックしない操作なので、要素が見つからないときは空の配列を返します。
|
324
|
+
狙った要素が追加を待ち合わせするにはread_tagを使います。
|
325
|
+
|
326
|
+
>||
|
327
|
+
ターミナル2
|
328
|
+
>> MyDrip.read_tag(0, 'sora_h.age')
|
329
|
+
||<
|
330
|
+
|
331
|
+
ブロックしますね。別の端末から値を設定してみます。
|
332
|
+
|
333
|
+
>||
|
334
|
+
ターミナル3
|
335
|
+
>> MyDrip.write(12, 'sora_h.age')
|
336
|
+
=> 1313359385886937
|
233
337
|
||<
|
234
338
|
|
235
|
-
|
339
|
+
read_tagのブロックは解けて、いま追加したオブジェクトが返ります。
|
236
340
|
|
237
341
|
>||
|
238
|
-
|
239
|
-
|
342
|
+
ターミナル2
|
343
|
+
>> MyDrip.read_tag(0, 'sora_h.age')
|
344
|
+
=> [[1313359385886937, 12, "sora_h.age"]]
|
240
345
|
||<
|
241
346
|
|
242
|
-
|
347
|
+
***実験のまとめ
|
348
|
+
|
349
|
+
タグをうまく使うとHashの基本操作である値の設定と取り出しが模倣できることがわかりました。Hashと違うところは次の点です。
|
350
|
+
- 要素は消せない
|
351
|
+
- 履歴がある
|
352
|
+
- keys/eachがない
|
353
|
+
Hashと違い要素を削除することはできませんが、nil、あるいは削除状態を表わす特別なオブジェクトを設定するなどによって代用できると思います。また、要素を削除できない副産物として変更の履歴を全て見ることができます。
|
354
|
+
keysとeachが用意されないのは意図してのことです。簡単に作れるので一度作成しましたが削除しました。現在、DripにそのAPIは残っていません。keysを実装するには全ての要素を一度集める必要がありますが、要素数が大きくなったときに破綻する可能性があるからです。多くの分散ハッシュテーブルでもkeysは用意されていないのではないかと思います。
|
355
|
+
|
356
|
+
TupleSpaceと似ている点があります。read_tagを使うと要素の追加や更新を待ち合わせることができます。これはRindaのTupleSpaceにおけるreadのパターンマッチングを非常に限定したものと考えられます。ある特定のタグをもつ要素が追加されるまでプロセスを待たせることができます。このパターンマッチはRindaと比較すると非常に貧弱なものですが、実際のアプリケーションの多くには充分ではないか予想しています。
|
357
|
+
DripではRindaで広げすぎた仕様を狭くして、最適化しやすい単純な仕組みに挑戦しています。Rindaはインメモリを中心にRuby的な豪華な世界を表現しました。これに対しDripでは永続化を前提として協調機構を考え直しより単純なもの目指しました。
|
358
|
+
この予想を検証するにはもっと多くのアプリケーションが必要ですね。
|
359
|
+
|
360
|
+
この二つの節ではQueue、Hashとの比較を通じてDripを説明してきました。単純な追記しかないストリームでもちょっと凝ったデータ構造が表現できそうです。多くのデータ構造においてeachが定義できるわけですから、世界のほとんどは一直線にならべることができるかもしれませんしね。
|
361
|
+
|
362
|
+
QueueやHashと比較してDripを説明しました。
|
363
|
+
|
364
|
+
**キーとブラウズ
|
365
|
+
|
366
|
+
ここではDripに格納されたデータをブラウズする方法を学びます。Dripでは全ての要素はwriteされた順に並んでいますから、Dripにおけるデータのブラウズは時間軸に沿って旅をするようなものです。
|
367
|
+
ブラウズに使うAPIのほとんどは注目点(カーソル)のキーを引数にとります。まずキーの規則を説明し、次にブラウズの実際を見ていきます。
|
368
|
+
|
369
|
+
***キー
|
370
|
+
|
371
|
+
Drip#writeすると、その要素に対応するキーが返ります。キーは単調増加の整数で、あたらしいキーはこれまでに格納されたどのキーよりも大きくなります。現在の実装ではキーは次のように計算されます。
|
372
|
+
|
373
|
+
>|ruby|
|
374
|
+
def time_to_key(time)
|
375
|
+
time.tv_sec * 1000000 + time.tv_usec
|
376
|
+
end
|
377
|
+
||<
|
378
|
+
|
379
|
+
キーは時刻から計算された整数です。64bitマシンにおいて(当面の間は)Fixnumです。usecの分解能しかありませんから、1 usecのうちに複数の要素を書き込める際に衝突が発生します。この場合、最も大きなキーより一つ大きな値が選択されます。
|
380
|
+
|
381
|
+
>|ruby|
|
382
|
+
# lastは最後の(最大の)キー
|
383
|
+
key = [time_to_key(at), last + 1].max
|
384
|
+
||<
|
385
|
+
|
386
|
+
0が最古のキーとなります。一番旧い要素を指定するときに思い出して下さい。
|
387
|
+
|
388
|
+
***ブラウズ
|
243
389
|
|
390
|
+
これまでの実験でread、read_tag、headを試しました。他にも次のようなAPIがあります。
|
391
|
+
- 未来方向へのブラウズ / read, read_tag, newer
|
392
|
+
- 過去方向へのブラウズ / head, older
|
393
|
+
DripのアプリケーションではこれらのAPIを使って時間軸を前後に移動します。タグをうまく使ってスキップすることもあります。
|
394
|
+
|
395
|
+
この節では、タグを使って任意の要素へシークしそこから順に読む操作を紹介します。
|
396
|
+
|
397
|
+
次の疑似コードは全ての要素を4つずつ読み出していく例です。kが注目点です。kをreadした要素の最後のキーとしながら繰り返すことで、要素を順に読んでいくことができます。
|
398
|
+
|
399
|
+
>|ruby|
|
400
|
+
while true
|
401
|
+
ary = drip.read(k, 4, 1)
|
402
|
+
...
|
403
|
+
k = ary[-1][0]
|
404
|
+
end
|
405
|
+
||<
|
406
|
+
|
407
|
+
この疑似コードをirbで分解しながら実行していきます。MyDripは動いていますか?この実験もMyDripを使います。まずirbからMyDripにテスト用のデータを書き込みます。
|
408
|
+
|
409
|
+
>||
|
244
410
|
ターミナル1
|
411
|
+
% irb -r my_drip --simple-prompt
|
412
|
+
>> MyDrip.write('sentinel', 'test1')
|
413
|
+
=> 1313573767321912
|
414
|
+
>> MyDrip.write(:orange, 'test1=orange')
|
415
|
+
=> 1313573806023712
|
416
|
+
>> MyDrip.write(:orange, 'test1=orange')
|
417
|
+
=> 1313573808504784
|
418
|
+
>> MyDrip.write(:blue, 'test1=blue')
|
419
|
+
=> 1313573823137557
|
420
|
+
>> MyDrip.write(:green, 'test1=green')
|
421
|
+
=> 1313573835145049
|
422
|
+
>> MyDrip.write(:orange, 'test1=orange')
|
423
|
+
=> 1313573840760815
|
424
|
+
>> MyDrip.write(:orange, 'test1=orange')
|
425
|
+
=> 1313573842988144
|
426
|
+
>> MyDrip.write(:green, 'test1=green')
|
427
|
+
=> 1313573844392779
|
428
|
+
||<
|
429
|
+
|
430
|
+
はじめに書いたのは実験を始めた時点を記録するための錨のような要素です。それ以降、オレンジ、オレンジ、青、緑、オレンジ、オレンジ、緑とオブジェクトをwriteしました。色の名前に対応したタグをつけてあります。
|
431
|
+
|
245
432
|
>||
|
246
|
-
|
433
|
+
ターミナル2
|
434
|
+
% irb -r my_drip --simple-prompt
|
435
|
+
>> k, = MyDrip.head(1, 'test1')[0]
|
436
|
+
=> [1313573767321912, "sentinel", "test1"]
|
437
|
+
>> k
|
438
|
+
=> 1313573767321912
|
247
439
|
||<
|
248
440
|
|
441
|
+
まず"test1"というタグをつけた錨の要素をキーを手に入れます。実験の出発点となります。headを使うのが良いでしょう。
|
442
|
+
|
443
|
+
次に錨の以降の要素を4つreadします。
|
444
|
+
|
445
|
+
>||
|
249
446
|
ターミナル2
|
447
|
+
>> ary = MyDrip.read(k, 4)
|
448
|
+
=> [[1313573806023712, :orange, "test1=orange"], [1313573808504784, :orange, "test1=orange"], [1313573823137557, :blue, "test1=blue"], [1313573835145049, :green, "test1=green"]]
|
449
|
+
||<
|
450
|
+
|
451
|
+
読めましたか?次に注目点を更新して、もう一度4つreadしてみましょう。
|
452
|
+
|
250
453
|
>||
|
251
|
-
|
454
|
+
ターミナル2
|
455
|
+
>> k = ary[-1][0]
|
456
|
+
=> 1313573835145049
|
457
|
+
>> ary = MyDrip.read(k, 4)
|
458
|
+
=> [[1313573840760815, :orange, "test1=orange"], [1313573842988144, :orange, "test1=orange"], [1313573844392779, :green, "test1=green"]]
|
252
459
|
||<
|
253
460
|
|
254
|
-
|
461
|
+
続きの3つの要素が返りました。これはk以降の要素が3つしかないからです。さらに読むとどうなるでしょう。みなさんの予想通り、readはブロックするはずです。
|
462
|
+
|
463
|
+
>||
|
464
|
+
ターミナル2
|
465
|
+
>> k = ary[-1][0]
|
466
|
+
=> 1313573844392779
|
467
|
+
>> ary = MyDrip.read(k, 4)
|
468
|
+
||<
|
469
|
+
|
470
|
+
別の端末からなにかwriteしてreadが動き出すか、確認します。
|
255
471
|
|
256
|
-
ターミナル3
|
257
472
|
>||
|
258
|
-
|
259
|
-
|
473
|
+
ターミナル1
|
474
|
+
>> MyDrip.write('hello')
|
475
|
+
=> 1313574622814421
|
260
476
|
||<
|
261
477
|
|
478
|
+
解除されましたか?
|
479
|
+
|
480
|
+
次はread_tagを使ってフィルタする例を示します。注目点を巻き戻してもう一度実験です。
|
481
|
+
|
482
|
+
>||
|
262
483
|
ターミナル2
|
484
|
+
>> k, = MyDrip.head(1, 'test1')[0]
|
485
|
+
=> [1313573767321912, "sentinel", "test1"]
|
486
|
+
||<
|
487
|
+
|
488
|
+
注目点より新しいデータで、タグが'test1=orange'のものを4つ(最低でも2つ)readせよ、としてみましょう。
|
489
|
+
|
263
490
|
>||
|
264
|
-
|
491
|
+
>> ary = MyDrip.read_tag(k, 'test1=orange', 4, 2)
|
492
|
+
=> [[1313573806023712, :orange, "test1=orange"], [1313573808504784, :orange, "test1=orange"], [1313573840760815, :orange, "test1=orange"], [1313573842988144, :orange, "test1=orange"]]
|
265
493
|
||<
|
266
494
|
|
267
|
-
|
495
|
+
オレンジばかり、4つ手に入りました。
|
496
|
+
|
497
|
+
注目点を更新して、もう一度同じ操作をしてみます。
|
268
498
|
|
499
|
+
>||
|
500
|
+
>> k = ary[-1][0]
|
501
|
+
=> 1313573842988144
|
502
|
+
>> ary = MyDrip.read_tag(k, 'test1=orange', 4, 2)
|
503
|
+
||<
|
504
|
+
|
505
|
+
新しい注目点よりも後には、オレンジの要素が一つもありませんからブロックします。別の端末からオレンジを2つwriteすれば、このread_tagは動き出すでしょう。
|
506
|
+
|
507
|
+
>||
|
508
|
+
ターミナル1
|
509
|
+
>> MyDrip.write('more orange', 'test1=orange')
|
510
|
+
=> 1313575076451864
|
511
|
+
>> MyDrip.write('more orange', 'test1=orange')
|
512
|
+
=> 1313575077963911
|
513
|
+
||<
|
514
|
+
|
515
|
+
>||
|
269
516
|
ターミナル2
|
517
|
+
>> ary = MyDrip.read_tag(k, 'test1=orange', 4, 2)
|
518
|
+
=> [[1313575076451864, "more orange", "test1=orange"], [1313575077963911, "more orange", "test1=orange"]]
|
519
|
+
||<
|
520
|
+
|
521
|
+
ここではタグを使ったシークと、そこからのread、フィルタを使ったread_tagの例を示しました。Dripのデータの中をブラウズする際の基本となるイディオムです。
|
522
|
+
|
523
|
+
ほかにもいくつかユーティリティメソッドがあります。
|
524
|
+
|
525
|
+
>|ruby|
|
526
|
+
Drip#newer(key, tag=nil)
|
527
|
+
||<
|
528
|
+
|
529
|
+
keyより新しいものを一つ返します。tagを指定することもできます。newerはread/read_tagのラッパーです。新しい要素が無い場合ブロックせず、nilを返します。
|
530
|
+
|
531
|
+
>|ruby|
|
532
|
+
Drip#older(key, tag=nil)
|
533
|
+
||<
|
534
|
+
|
535
|
+
keyよりも旧いものを一つ返します。tagを指定することもできます。新しい要素が無い場合ブロックせず、nilを返します。
|
536
|
+
|
537
|
+
そうそう。普通過ぎてわすれていましたが、キーがわかっているときに対応する値を取り出すAPIもあります。
|
538
|
+
|
539
|
+
>|ruby|
|
540
|
+
Drip#[](key)
|
541
|
+
||<
|
542
|
+
|
270
543
|
>||
|
271
|
-
>>
|
272
|
-
=> [
|
544
|
+
>> k, = MyDrip.head(1, 'test1')[0]
|
545
|
+
=> [1313573767321912, "sentinel", "test1"]
|
546
|
+
>> MyDrip[k]
|
547
|
+
=> ["sentinel", "test1"]
|
273
548
|
||<
|
274
549
|
|
275
|
-
|
550
|
+
値とタグからなる配列を返します。キーは返りません。
|
551
|
+
|
552
|
+
これで主要なAPIの説明は終わりです。そういえばRubyでよく見かけるeachはありませんでしたね。それについてはつぎのクドい話を読んで下さい。
|
276
553
|
|
277
554
|
**APIの設計指針
|
278
555
|
|
@@ -285,96 +562,697 @@ readでは、自分の知らない情報を一度に最大n個、少なくとも
|
|
285
562
|
|
286
563
|
Dripはストレージに関する一連の習作の経験から、「作りすぎない」ことに留意しました。「作る」ことは楽しいので、請われるままに機能を増やしてしまうことがしばしば起こります(私はそういう経験があります)。Dripのポリシーを明確にして、機能を増やしてしまう誘惑と戦いました。
|
287
564
|
|
288
|
-
|
565
|
+
**アプリケーション
|
566
|
+
|
567
|
+
***簡易検索システム
|
568
|
+
|
569
|
+
ここでは非常に小さな検索システムを作ります。検索システムのミニチュアの作成を通じてDripの応用のヒントとして下さい。
|
570
|
+
このシステムには主に三つのプロセスが登場します。自分のマシンにあるRubyスクリプトを探してはDripに登録するクロウラ、Dripへ登録されたファイルを検索のために索引をつけるインデクサ、そして中心となるMyDripサーバです。
|
571
|
+
|
572
|
+
|
573
|
+
***動かし方
|
574
|
+
|
575
|
+
この実験でもMyDripを使用しますので、事前にMyDrip.invokeするか、Windows環境では代替となるサーバを起動しておいて下さいね。
|
576
|
+
|
577
|
+
>||
|
578
|
+
$ irb -r drip -r my_drip
|
579
|
+
>> MyDrip.invoke
|
580
|
+
=> 45616
|
581
|
+
||<
|
582
|
+
|
583
|
+
|
584
|
+
今回のサンプルはDripのソースコードの中にも含まれています。まずはダウンロードしてみましょう。
|
585
|
+
|
586
|
+
>||
|
587
|
+
$ cd ~
|
588
|
+
$ git clone git://github.com/seki/Drip.git
|
589
|
+
$ cd Drip/sample/demo4book
|
590
|
+
||<
|
591
|
+
|
592
|
+
実際にcrawlerを動かす前にcrawler.rbの10行目に、検索したいディレクトリして下さい。
|
593
|
+
ファイル数が多いと実験に時間が非常にかかるので、少ないディレクトリを選んでください。500ファイル程度が実験しやすいのではないかと思います。今回はソースコードのディレクトリを指定しました。
|
594
|
+
|
595
|
+
>||
|
596
|
+
@root = File.expand_path('~/Drip/')
|
597
|
+
||<
|
598
|
+
|
599
|
+
以下のようにcrawl.rbを実行するとcrawlするごとにファイルの一覧が表示されます。
|
600
|
+
|
601
|
+
>||
|
602
|
+
$ ruby crawl.rb
|
603
|
+
["install.rb",
|
604
|
+
"lib/drip/version.rb",
|
605
|
+
"lib/drip.rb",
|
606
|
+
"lib/my_drip.rb",
|
607
|
+
"sample/copocopo.rb",
|
608
|
+
"sample/demo4book/crawl.rb",
|
609
|
+
"sample/demo4book/index.rb",
|
610
|
+
"sample/drip_s.rb",
|
611
|
+
"sample/drip_tw.rb",
|
612
|
+
"sample/gca.rb",
|
613
|
+
"sample/hello_tw.rb",
|
614
|
+
"sample/my_status.rb",
|
615
|
+
"sample/simple-oauth.rb",
|
616
|
+
"sample/tw_markov.rb",
|
617
|
+
"test/basic.rb"]
|
618
|
+
||<
|
619
|
+
|
620
|
+
次に別のターミナルでインデクサを起動し、探したい単語を入力すると、その単語が存在するファイル名を一覧として表示します。
|
621
|
+
ここでは「def」という単語を検索しています。起動してすぐはまだ索引が完全でないので、急いでなんども検索すると索引対象が増えていく様子を見られるかもしれません。
|
622
|
+
|
623
|
+
>||
|
624
|
+
$ ruby index.rb
|
625
|
+
def
|
626
|
+
["sample/demo4book/index.rb", "sample/demo4book/crawl.rb"]
|
627
|
+
2
|
628
|
+
def
|
629
|
+
["sample/drip_s.rb",
|
630
|
+
"lib/drip.rb",
|
631
|
+
"lib/my_drip.rb",
|
632
|
+
"sample/copocopo.rb",
|
633
|
+
"sample/demo4book/index.rb",
|
634
|
+
"sample/demo4book/crawl.rb"]
|
635
|
+
6
|
636
|
+
||<
|
637
|
+
|
638
|
+
クロウラは60秒置きに更新を調べるようになっています。標準入力からなにか入力すると、更新の合間を待ってから終了します。これは、一般的な検索システムのクロウラを模倣して、適度に休むようにしてあります。とくにWebページなど検索対象が広い場合などは頻繁な更新情報の収集にはムリがあります。
|
639
|
+
なお、クローラを休ませる時間を短くすればファイルを更新してすぐに索引に反映されるようになります。このクローラを改造していくことで、自分だけのちょっとしたリアルタイム検索ツールになるかもしれません。また最近のOSでしたらファイルの更新自体をイベントとして知ることができると思うので、そういった機構をトリガーとするのも面白いと思います。
|
640
|
+
|
641
|
+
ここからはソースコードを解説していきます。
|
642
|
+
|
643
|
+
***投入する要素
|
644
|
+
|
645
|
+
このシステムでDripに投入するオブジェクトとタグについて説明します。主に使用するのは「ファイル更新通知」です。
|
646
|
+
|
647
|
+
-ファイル更新通知 - ファイル名、内容、更新日の配列です。'rbcrawl'と'rbcrawl-fname=ファイル名'の二つのタグを持ちます。
|
648
|
+
|
649
|
+
クロウラは更新されたファイルを見つけるたびにこの情報をwriteします。これはファイル内容のアーカイブであると同時に、更新を通知するイベントになります。インデクサは更新通知を見つけるたびに索引を更新します。
|
650
|
+
|
651
|
+
補助的に利用するものもあります。
|
652
|
+
|
653
|
+
-クロウラの足跡 - ひとまとまりの処理のなかで更新したファイル名の一覧と、その時刻をメモします。'rbcrawl-footprint'というタグを持ちます。
|
654
|
+
-実験開始を示すアンカー - 'rbcrawl-begin'というタグを持ちます。何度か実験を繰り返しているうちにはじめからやり直したくなったらこのタグでなにかwriteしてください。
|
655
|
+
|
656
|
+
ではこれらのオブジェクトやタグがどのように使われているか見てみましょう
|
657
|
+
|
658
|
+
***クロウラ
|
659
|
+
|
660
|
+
簡易クロウラの動作を説明します。
|
661
|
+
|
662
|
+
>|ruby|
|
663
|
+
class Crawler
|
664
|
+
include MonitorMixin
|
665
|
+
|
666
|
+
def initialize
|
667
|
+
super()
|
668
|
+
@root = File.expand_path('~/develop/git-repo/')
|
669
|
+
@drip = MyDrip
|
670
|
+
k, = @drip.head(1, 'rbcrawl-begin')[0]
|
671
|
+
@fence = k || 0
|
672
|
+
end
|
673
|
+
|
674
|
+
def last_mtime(fname)
|
675
|
+
k, v, = @drip.head(1, 'rbcrawl-fname=' + fname)[0]
|
676
|
+
(v && k > @fence) ? v[1] : Time.at(1)
|
677
|
+
end
|
678
|
+
|
679
|
+
def do_crawl
|
680
|
+
synchronize do
|
681
|
+
ary = []
|
682
|
+
Dir.chdir(@root)
|
683
|
+
Dir.glob('**/*.rb').each do |fname|
|
684
|
+
mtime = File.mtime(fname)
|
685
|
+
next if last_mtime(fname) >= mtime
|
686
|
+
@drip.write([fname, mtime, File.read(fname)],
|
687
|
+
'rbcrawl', 'rbcrawl-fname=' + fname)
|
688
|
+
ary << fname
|
689
|
+
end
|
690
|
+
@drip.write(ary, 'rbcrawl-footprint')
|
691
|
+
ary
|
692
|
+
end
|
693
|
+
end
|
694
|
+
|
695
|
+
def quit
|
696
|
+
synchronize do
|
697
|
+
exit(0)
|
698
|
+
end
|
699
|
+
end
|
700
|
+
end
|
701
|
+
||<
|
702
|
+
|
703
|
+
まず、指定したディレクトリ(@root)以下にある*.rbのファイルを探します。そしてその更新時刻を調べ、新しいファイルを見つけたらその内容や時刻をwriteします。
|
704
|
+
これは実際には以下のようなデータを書き込んでいます。
|
705
|
+
|
706
|
+
>|ruby|
|
707
|
+
@drip.write(
|
708
|
+
["sample/demo4book/index.rb", 2011-08-23 23:50:44 +0100, "ファイルの中身"],
|
709
|
+
"rbcrawl", "rbcrawl-fname=sample/demo4book/index.rb"
|
710
|
+
)
|
711
|
+
||<
|
712
|
+
|
713
|
+
値はファイル名、時刻、ファイルの中身からなる配列で、それに対して二つのタグがついています。
|
714
|
+
|
715
|
+
クロウラは60秒置きに更新を調べるようになっています。標準入力からなにか入力すると、更新の合間を待ってから終了します。一回の処理で見つけたファイル名の配列を'rbcrawl-footprint'というタグをつけて覚えておきます。たとえば、以下のようなデータを書き込みます。
|
716
|
+
|
717
|
+
>|ruby|
|
718
|
+
@drip.write(["sample/demo4book/index.rb"], 'rbcrawl-footprint')
|
719
|
+
||<
|
720
|
+
|
721
|
+
このバージョンのクロウラはファイルの削除を追いかけませんが、この足跡情報を使えば削除を知ることができるかもしれません。
|
722
|
+
|
723
|
+
更新されたか否かは、headメソッドで一つ前のバージョンを探し比較して検査します。
|
724
|
+
'rbcrawl-fname=ファイル名'というタグでheadすることで、直前のバージョン(つまりDripに書かれている最新のバージョン)を調べることができます。
|
725
|
+
|
726
|
+
>|ruby|
|
727
|
+
k, v = @drip.head(1, "rbcrawl-fname=sample/demo4book/index.rb")[0]
|
728
|
+
||<
|
729
|
+
|
730
|
+
以下に完全なクロウラを載せます。
|
731
|
+
|
732
|
+
>|ruby|
|
733
|
+
require 'pp'
|
734
|
+
require 'my_drip'
|
735
|
+
require 'monitor'
|
736
|
+
|
737
|
+
class Crawler
|
738
|
+
include MonitorMixin
|
739
|
+
|
740
|
+
def initialize
|
741
|
+
super()
|
742
|
+
@root = File.expand_path('~/develop/git-repo/')
|
743
|
+
@drip = MyDrip
|
744
|
+
k, = @drip.head(1, 'rbcrawl-begin')[0]
|
745
|
+
@fence = k || 0
|
746
|
+
end
|
747
|
+
|
748
|
+
def last_mtime(fname)
|
749
|
+
k, v, = @drip.head(1, 'rbcrawl-fname=' + fname)[0]
|
750
|
+
(v && k > @fence) ? v[1] : Time.at(1)
|
751
|
+
end
|
752
|
+
|
753
|
+
def do_crawl
|
754
|
+
synchronize do
|
755
|
+
ary = []
|
756
|
+
Dir.chdir(@root)
|
757
|
+
Dir.glob('**/*.rb').each do |fname|
|
758
|
+
mtime = File.mtime(fname)
|
759
|
+
next if last_mtime(fname) >= mtime
|
760
|
+
@drip.write([fname, mtime, File.read(fname)],
|
761
|
+
'rbcrawl', 'rbcrawl-fname=' + fname)
|
762
|
+
ary << fname
|
763
|
+
end
|
764
|
+
@drip.write(ary, 'rbcrawl-footprint')
|
765
|
+
ary
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
def quit
|
770
|
+
synchronize do
|
771
|
+
exit(0)
|
772
|
+
end
|
773
|
+
end
|
774
|
+
end
|
775
|
+
|
776
|
+
if __FILE__ == $0
|
777
|
+
crawler = Crawler.new
|
778
|
+
Thread.new do
|
779
|
+
while true
|
780
|
+
pp crawler.do_crawl
|
781
|
+
sleep 60
|
782
|
+
end
|
783
|
+
end
|
784
|
+
|
785
|
+
gets
|
786
|
+
crawler.quit
|
787
|
+
end
|
788
|
+
||<
|
789
|
+
|
289
790
|
|
290
|
-
|
791
|
+
|
792
|
+
***インデクサ
|
793
|
+
|
794
|
+
このインデクサは索引の作成、更新と、検索そのものも提供します。指定した単語を含んでいるファイルの名前を返します。このサンプルは実験用のミニチュアなので、インメモリに索引を作ることにしました。rbtreeが必要ですが、Dripが動いているならrbtreeはインストールされていると思います。
|
291
795
|
|
292
796
|
>|ruby|
|
293
|
-
|
797
|
+
class Indexer
|
798
|
+
def initialize(cursor=0)
|
799
|
+
@drip = MyDrip
|
800
|
+
@dict = Dict.new
|
801
|
+
k, = @drip.head(1, 'rbcrawl-begin')[0]
|
802
|
+
@fence = k || 0
|
803
|
+
@cursor = [cursor, @fence].max
|
804
|
+
end
|
805
|
+
attr_reader :dict
|
806
|
+
|
807
|
+
def update_dict
|
808
|
+
each_document do |cur, prev|
|
809
|
+
@dict.delete(*prev) if prev
|
810
|
+
@dict.push(*cur)
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
def each_document
|
815
|
+
while true
|
816
|
+
ary = @drip.read_tag(@cursor, 'rbcrawl', 10, 1)
|
817
|
+
ary.each do |k, v|
|
818
|
+
prev = prev_version(k, v[0])
|
819
|
+
yield(v, prev)
|
820
|
+
@cursor = k
|
821
|
+
end
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
def prev_version(cursor, fname)
|
826
|
+
k, v = @drip.older(cursor, 'rbcrawl-fname=' + fname)
|
827
|
+
(v && k > @fence) ? v : nil
|
828
|
+
end
|
829
|
+
end
|
294
830
|
||<
|
295
831
|
|
296
|
-
Drip
|
297
|
-
また、readとread_tagのキーは共通ですから二つを組み合わせることもできます。例えば、read_tagで狙ったタグを持つ要素を取得して、それ以降の要素をreadで順に全て集める、といった操作です。
|
832
|
+
インデクサはDripから'rbcrawl'タグのついたオブジェクトを取り出し、その都度、索引を更新します。
|
298
833
|
|
299
|
-
|
834
|
+
>|ruby|
|
835
|
+
@drip.read_tag(@cursor, 'rbcrawl', 10, 1)
|
836
|
+
||<
|
300
837
|
|
301
|
-
|
838
|
+
第4引数の「1」に注目して下さい。先ほど「keyより新しい要素の数がat_least個に満たない場合は、新しいデータが追加されるまでブロックします」と説明したのを覚えていますか?一度に10個ずつ、最低でも1個ずつ返せ、という指示ですから返せる要素が一つもないときにはブロックします。
|
839
|
+
これによりクロウラが'rbcrawl'タグのデータを挿入するのをブロックしながら待ち合わせている事になります。
|
840
|
+
|
841
|
+
インデクサにとってrbcrawlタグのオブジェクトは更新イベントであると同時に文書でもあります。更新されたファイル名、更新時刻、内容がまとめて手に入ります。
|
842
|
+
また、DripはQueueとちがい、すでに読んだ要素を再び読むことが可能です。注目点の直前の要素を調べるolderなどで調べることが可能です。
|
302
843
|
|
303
844
|
>|ruby|
|
304
|
-
|
845
|
+
def prev_version(cursor, fname)
|
846
|
+
k, v = @drip.older(cursor, 'rbcrawl-fname=' + fname)
|
847
|
+
(v && k > @fence) ? v : nil
|
848
|
+
end
|
849
|
+
||<
|
850
|
+
|
851
|
+
通知されたファイルに旧いバージョンの文書があった場合、インデクサは旧い内容を使って索引を削除してから、新しい内容で索引を追加します。
|
305
852
|
|
306
|
-
|
853
|
+
>|ruby|
|
854
|
+
def update_dict
|
855
|
+
each_document do |cur, prev|
|
856
|
+
@dict.delete(*prev) if prev
|
857
|
+
@dict.push(*cur)
|
858
|
+
end
|
859
|
+
end
|
307
860
|
||<
|
308
861
|
|
309
|
-
|
310
|
-
|
862
|
+
インデクサは起動されるとスレッドを生成してサブスレッドでDripからのread_tagと索引づけを行います。
|
863
|
+
|
864
|
+
>|ruby|
|
865
|
+
indexer ||= Indexer.new(0)
|
866
|
+
Thread.new do
|
867
|
+
indexer.update_dict
|
868
|
+
end
|
869
|
+
||<
|
311
870
|
|
312
|
-
|
871
|
+
メインスレッドではユーザーからの入力を待ち、入力されるとその単語を探して検索結果を印字します。
|
313
872
|
|
314
|
-
|
873
|
+
>|ruby|
|
874
|
+
while line = gets
|
875
|
+
ary = indexer.dict.query(line.chomp)
|
876
|
+
pp ary
|
877
|
+
pp ary.size
|
878
|
+
end
|
879
|
+
||<
|
315
880
|
|
316
|
-
|
881
|
+
以下に完全なインデクサを載せます。
|
317
882
|
|
318
883
|
>|ruby|
|
319
|
-
|
884
|
+
require 'nkf'
|
885
|
+
require 'rbtree'
|
886
|
+
require 'my_drip'
|
887
|
+
require 'monitor'
|
888
|
+
require 'pp'
|
889
|
+
|
890
|
+
|
891
|
+
class Indexer
|
892
|
+
def initialize(cursor=0)
|
893
|
+
@drip = MyDrip
|
894
|
+
@dict = Dict.new
|
895
|
+
k, = @drip.head(1, 'rbcrawl-begin')[0]
|
896
|
+
@fence = k || 0
|
897
|
+
@cursor = [cursor, @fence].max
|
898
|
+
end
|
899
|
+
attr_reader :dict
|
900
|
+
|
901
|
+
def update_dict
|
902
|
+
each_document do |cur, prev|
|
903
|
+
@dict.delete(*prev) if prev
|
904
|
+
@dict.push(*cur)
|
905
|
+
end
|
906
|
+
end
|
907
|
+
|
908
|
+
def each_document
|
909
|
+
while true
|
910
|
+
ary = @drip.read_tag(@cursor, 'rbcrawl', 10, 1)
|
911
|
+
ary.each do |k, v|
|
912
|
+
prev = prev_version(k, v[0])
|
913
|
+
yield(v, prev)
|
914
|
+
@cursor = k
|
915
|
+
end
|
916
|
+
end
|
917
|
+
end
|
918
|
+
|
919
|
+
def prev_version(cursor, fname)
|
920
|
+
k, v = @drip.older(cursor, 'rbcrawl-fname=' + fname)
|
921
|
+
(v && k > @fence) ? v : nil
|
922
|
+
end
|
923
|
+
end
|
924
|
+
|
925
|
+
class Dict
|
926
|
+
include MonitorMixin
|
927
|
+
def initialize
|
928
|
+
super()
|
929
|
+
@tree = RBTree.new
|
930
|
+
end
|
931
|
+
|
932
|
+
def query(word)
|
933
|
+
synchronize do
|
934
|
+
@tree.bound([word, 0, ''], [word + "\0", 0, '']).collect {|k, v| k[2]}
|
935
|
+
end
|
936
|
+
end
|
937
|
+
|
938
|
+
def delete(fname, mtime, src)
|
939
|
+
synchronize do
|
940
|
+
each_tree_key(fname, mtime, src) do |key|
|
941
|
+
@tree.delete(key)
|
942
|
+
end
|
943
|
+
end
|
944
|
+
end
|
945
|
+
|
946
|
+
def push(fname, mtime, src)
|
947
|
+
synchronize do
|
948
|
+
each_tree_key(fname, mtime, src) do |key|
|
949
|
+
@tree[key] = true
|
950
|
+
end
|
951
|
+
end
|
952
|
+
end
|
953
|
+
|
954
|
+
def intern(word)
|
955
|
+
k, v = @tree.lower_bound([word, 0, ''])
|
956
|
+
return k[0] if k && k[0] == word
|
957
|
+
word
|
958
|
+
end
|
959
|
+
|
960
|
+
def each_tree_key(fname, mtime, src)
|
961
|
+
NKF.nkf('-w', src).scan(/\w+/m).uniq.each do |word|
|
962
|
+
yield([intern(word), mtime.to_i, fname])
|
963
|
+
end
|
964
|
+
end
|
965
|
+
end
|
966
|
+
|
967
|
+
if __FILE__ == $0
|
968
|
+
indexer ||= Indexer.new(0)
|
969
|
+
Thread.new do
|
970
|
+
indexer.update_dict
|
971
|
+
end
|
972
|
+
|
973
|
+
while line = gets
|
974
|
+
ary = indexer.dict.query(line.chomp)
|
975
|
+
pp ary
|
976
|
+
pp ary.size
|
977
|
+
end
|
978
|
+
end
|
320
979
|
||<
|
321
980
|
|
322
|
-
|
981
|
+
***クロウラの動作間隔とインデクサの同期
|
982
|
+
|
983
|
+
このサンプルで示したかったものの一つに、複数の処理が自分の都合のよいタイミングで動作するというものがあります。
|
984
|
+
|
985
|
+
クロウラは定期的に動作を開始します。クロウラはインデクサの状態など気にせずに処理を行い、更新を見つけてはwriteします。
|
986
|
+
インデクサも同様です。インデクサはクロウラの動作状況を気にせず、これまでDripに格納されていた文書をまとめて取り出しては索引の更新を行います。文書を処理し終わったら、新しい文書がwriteされるまで休眠状態になります。
|
323
987
|
|
988
|
+
データの流れとしては、クロウラが発生源で、Dripに蓄えられて、インデクサがそれを取り出し索引を作ります。しかし、クロウラが発生させた処理の中でインデクサが動作するわけではありません。たとえば、オブザーバーパターンでクロウラ→インデクサとコールバック等のメソッド呼び出しの連鎖のなかで索引更新が行われると想像してみてください。クロウラ側の更新を調べる処理は、索引の更新と直列に動作し律速してしまいます。
|
989
|
+
Dripにおけるイベントの通知は、受動的ではありません。リスナ側が自分の都合のよいときに能動的に行われます。このスタイルはアクターモデルともよく似ています。インデクサは自分の担当する仕事が一通り終わって、自分の状態が安定してから次の文書を取り出します。dRubyのRMIがサブスレッドにより気付かないうちに実行されるのと対照的ですね。
|
324
990
|
|
991
|
+
ややこしい喩え話はともかく、クロウラはインデクサの処理を待つことなく動きますし、インデクサはクロウラの処理の頻度と関係なく自分のペースで動きます。Dripはメッセージングのミドルウェアとして彼らの間をゆるく仲介します。
|
325
992
|
|
326
|
-
|
993
|
+
***フェンスと足跡
|
327
994
|
|
328
|
-
Drip
|
995
|
+
実験を繰り返していると、最初の状態からやり直したくなることがあるでしょう。Dripのデータベースを作り直せばやりなおせますが、でもMyDripはこのアプリケーション以外からも雑多な情報をwriteされているでそれは抵抗がありますよね。
|
996
|
+
そこでこのアプリケーションの始まりの点を閉めすオブジェクトを導入することに。'rbcrawl-begin'というタグを持つオブジェクトがあるときは、それよりも旧い情報を無視することで、それ以前のオブジェクトに影響されずに実験できます。@fenceはクロウラ、インデクサのどちらでも使っているので読んでみて下さい。
|
997
|
+
具体的にはolderやheadの際にそのキーをチェックして、@fenceよりも旧かったら無視することにします。
|
329
998
|
|
999
|
+
>||
|
1000
|
+
=> MyDrip.write('fence', 'rbcrawl-begin')
|
1001
|
+
>> 1313573767321913
|
1002
|
+
||<
|
1003
|
+
|
1004
|
+
インデクサが索引を二次記憶に書くようになると、プロセスの寿命と索引の寿命が異なるようになります。このような状況にはしばしば出会うと思います。このとき、インデクサが処理を進めたポイントに足跡となるオブジェクトを残すことで、次回の起動に備えることができます。先のフェンスは無効となるポイントを示しましたが、この場合の足跡はまだ処理していないポイントを示すことになります。
|
1005
|
+
|
1006
|
+
|
1007
|
+
***RBTree
|
1008
|
+
|
1009
|
+
ここまではcrawlerとindexerがDripのタグや待ち合わせ機能をつかって、どのように新しい文章をインデックスに更新させるかについて説明してきました。
|
1010
|
+
でも実際の検索用インデックスがどのように作られているかにも興味ありませんか?
|
1011
|
+
|
1012
|
+
先に示したインデクサはRBTreeという拡張ライブラリを利用しています。RBTreeは赤黒木という検索に適した二分木のデータ構造とアルゴリズムを提供します。RubyのTreeではなく、red-black treeの略と思われます。Hashはハッシュ関数という魔法の関数を用意して、キーとなるオブジェクトからハッシュ値へ変換し、値を探します。RBTreeでは常にソート済みの列(実装は木だけど、木としてアクセスするAPIは用意されない)を準備しておき、二分探索を使って値を探します。「並んでいる」という性質を利用するといろいろおもしろいことができます。
|
1013
|
+
|
1014
|
+
本の索引を見て下さい。単語ごとにそれが出現する場所(本ならページ番号)が複数並んでいますよね。Hashで実装すると、ほぼこのままに表現できます。
|
1015
|
+
|
1016
|
+
>|ruby|
|
1017
|
+
class Dict
|
1018
|
+
def initialize
|
1019
|
+
@hash = Hash.new {|h, k| h[k] = Array.new}
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
def push(fname, words)
|
1023
|
+
words.each {|w| @hash[w] << fname}
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
def query(word, &blk)
|
1027
|
+
@hash[word].each(&blk)
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
dict = Dict.new
|
1032
|
+
dict.push('lib/drip.rb', ['def', 'Drip'])
|
1033
|
+
dict.push('lib/foo.rb', ['def'])
|
1034
|
+
dict.push('lib/bar.rb', ['def', 'bar', 'Drip'])
|
1035
|
+
dict.query('def') {|x| puts x}
|
1036
|
+
||<
|
1037
|
+
|
1038
|
+
ファイルが更新されたあとに行われる、二巡目の索引処理ではどうでしょう。
|
1039
|
+
旧くなった索引の削除や新しい索引の登録にはHashの中のArrayを全て読まなくてはなりません。これに対応するには、内側のArrayをHashにすれば効率よくなります。
|
1040
|
+
|
1041
|
+
>|ruby|
|
1042
|
+
class Dict2
|
1043
|
+
def initialize
|
1044
|
+
@hash = Hash.new {|h, k| h[k] = Hash.new}
|
1045
|
+
end
|
1046
|
+
|
1047
|
+
def push(fname, words)
|
1048
|
+
words.each {|w| @hash[w][fname] = true}
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
def query(word)
|
1052
|
+
@hash[word].each {|k, v| yield(k)}
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
||<
|
330
1056
|
|
331
|
-
|
1057
|
+
入れ子のHashのキーを使って索引を表現することができました。値は使い途がなくなってしまったところが興味深いです。入れ子のHashはなんだかツリー構造みたいですね。
|
332
1058
|
|
333
|
-
|
334
|
-
|
335
|
-
オブジェクトにはStringで表現したタグを複数つけることができます。同じタグを持つオブジェクトの集合を、時系列にブラウズすることができます。タグの付け方を工夫することで、履歴付きのKVSのように使うことができます。
|
336
|
-
オブジェクトを書くためのAPIはwriteメソッドです。引数は保存したいオブジェクトとそのタグです。writeはオブジェクトのキーを返します。ひとつのDripの中ではキーはユニークです。あるキーに関連しているオブジェクトはただ一つです。
|
337
|
-
オブジェクトを読むためのAPIは複数用意されています。基本となるのは、あるキーよりも後のオブジェクトを複数取得するreadメソッドです。アプリケーションは自分の知っているキーよりも後に追加されたオブジェクトをまとめて取得できます。もし、指定するキーよりも後にオブジェクトが追加されていなければ、このメソッドはブロックされ新しいオブジェクトが届くのを待ちます。
|
1059
|
+
RBTreeもHashと同様のAPIを提供していますから、上記のHashをRBTreeに置き換えて索引を表現することも可能ですが、もっとRBTreeらしい作戦を紹介します。
|
1060
|
+
二つ目のHashの例では入れ子のHashのキーを使いましたが、これをもう少し発展させましょう。単語と出現場所(ファイル名)をキーとします。入れ子のHashが組み立てていたツリー構造をフラットにしたようなもの、と言えます。
|
338
1061
|
|
339
|
-
|
1062
|
+
>|ruby|
|
1063
|
+
require 'rbtree'
|
340
1064
|
|
341
|
-
|
1065
|
+
class Dict3
|
1066
|
+
def initialize
|
1067
|
+
@tree = RBTree.new
|
1068
|
+
end
|
342
1069
|
|
343
|
-
|
1070
|
+
def push(fname, words)
|
1071
|
+
words.each {|w| @tree[[w, fname]] = true}
|
1072
|
+
end
|
344
1073
|
|
345
|
-
|
346
|
-
|
1074
|
+
def query(word)
|
1075
|
+
@tree.bound([word, ''], [word + "\0", '']) {|k, v| yield(k[1])}
|
1076
|
+
end
|
1077
|
+
end
|
1078
|
+
||<
|
347
1079
|
|
348
|
-
|
1080
|
+
queryメソッドで使用しているboundは、二つのキーの内側にある要素を調べるメソッドです。lowerとupperを指定します。
|
1081
|
+
ある単語を含むキーの最小値と、ある単語を含むキーの最大値を指定すれば、その単語の索引が手に入りますね。最小値は、一つ目の要素が対象の単語で、二つ目の要素が最も小さな文字列、つまり''で構成された配列です。では最も大きな文字列(何と比較しても大きい文字列)はなんでしょう。ちょっと思いつきませんね。代わりに「目的の単語の直後の単語を含むキーの最小値」を使います。RubyのStringには"\0"を含めることができますから、ある文字列よりも大きい最小の文字列は "\0" を連結したものと言えます。ちょっとトリッキーですね。そういう汚いものはメソッドに隠してしまいましょう。
|
349
1082
|
|
350
|
-
|
1083
|
+
>|ruby|
|
1084
|
+
def query(word)
|
1085
|
+
@tree.bound([word, ''], [word + "\0", '']) {|k, v| yield(k[1])}
|
1086
|
+
end
|
1087
|
+
||<
|
351
1088
|
|
1089
|
+
この例では単語の出現場所の識別子はファイル名です。先ほどのインデクサではドキュメントのIDとしてファイルの更新時刻とファイル名を用いました。さらに出現した行の番号を覚えたらどうなるか、などいろいろなバリエーションを想像してキーを考えるのも楽しいでしょう。
|
352
1090
|
|
353
|
-
|
1091
|
+
boundの仲間にはlower_bound、upper_boundというバリエーションもあります。狙ったキーの直前、直後(そのキーを含みます。以上、以下みたいな感じ。)などを調べられます。並んでいるキーとlower_boundを使ってand検索やor検索も効率よく行えます。次のコード片はand検索を行うものです。二つのカーソルを使い、カーソルが一致したときがand成功、カーソルが異なる場合には後側の単語のカーソルを先行する単語のカーソルの点からlower_boundさせます。これを繰り返すと、スキップしながらand検索が可能です。
|
354
1092
|
|
355
|
-
|
356
|
-
実運用でのバッチ処理アプリケーションなどでは、(とくに開発中など)処理が途中で失敗してしまったり、もっと効率のよいアルゴリズムを思いついてしまったりすることが珍しくありません。そんなとき、処理がうまく進んだ場所をメモしておいて、そこから再開することができます。また、失敗時に待ち行列から要素を紛失してしまうことも避けられます。
|
1093
|
+
次のスクリプトは、lower_boundを使ったand検索のアルゴリズムを実験するものです。起動引数に与えたファイルの中から'def'と'initialize'が同時に出現する行を探します。文書の「位置」はこのケースでは「ファイル名」「行番号」を選びました。
|
357
1094
|
|
358
1095
|
>|ruby|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
1096
|
+
require 'rbtree'
|
1097
|
+
require 'nkf'
|
1098
|
+
|
1099
|
+
class Query2
|
1100
|
+
def initialize
|
1101
|
+
@tree = RBTree.new
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
def push(word, fname, lineno)
|
1105
|
+
@tree[[word, fname, lineno]] = true
|
1106
|
+
end
|
1107
|
+
|
1108
|
+
def fwd(w1, fname, lineno)
|
1109
|
+
k, v = @tree.lower_bound([w1, fname, lineno])
|
1110
|
+
return nil unless k
|
1111
|
+
return nil unless k[0] == w1
|
1112
|
+
k[1..2]
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
def query2(w1, w2)
|
1116
|
+
f1 = fwd(w1, '', 0)
|
1117
|
+
f2 = fwd(w2, '', 0)
|
1118
|
+
while f1 && f2
|
1119
|
+
cmp = f1 <=> f2
|
1120
|
+
if cmp > 0
|
1121
|
+
f2 = fwd(w2, *f1)
|
1122
|
+
elsif cmp < 0
|
1123
|
+
f1 = fwd(w1, *f2)
|
1124
|
+
else
|
1125
|
+
yield(f1)
|
1126
|
+
f1 = fwd(w1, f1[0], f1[1] + 1)
|
1127
|
+
f2 = fwd(w2, f2[0], f2[1] + 1)
|
1128
|
+
end
|
365
1129
|
end
|
366
1130
|
end
|
367
1131
|
end
|
1132
|
+
|
1133
|
+
if __FILE__ == $0
|
1134
|
+
q2 = Query2.new
|
1135
|
+
|
1136
|
+
while line = ARGF.gets
|
1137
|
+
NKF.nkf('-w', line).scan(/\w+/) do |word|
|
1138
|
+
q2.push(word, ARGF.filename, ARGF.lineno)
|
1139
|
+
end
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
q2.query2('def', 'initialize') {|x| p x}
|
1143
|
+
end
|
368
1144
|
||<
|
369
1145
|
|
1146
|
+
boundでなくlower_bound、upper_boundを使うメリットはもう一つあります。
|
1147
|
+
boundの場合、その範囲に入っている要素の数が大きいとき、それだけのArrayをメモリに作ってしまいますが、lower_boundによって少しずつスコープを動かしていけば検索の回数は増えますが、一度に使用するメモリ、RMIであればそのためのバッファも減らすことができます。
|
1148
|
+
|
1149
|
+
順序のあるデータ構造、RBTreeは、実はDripの内部でも使われています。基本となるのはDripのキー(整数のキー)をそのまま使う集合です。もう一つ、タグのための集合にもRBTreeを使っています。この集合は[タグ(String), キー(Integer)]という配列をキーにします。
|
1150
|
+
|
1151
|
+
>||
|
1152
|
+
['rbcrawl-begin', 100030]
|
1153
|
+
['rbcrawl-begin', 103030]
|
1154
|
+
['rbcrawl-fname=a.rb', 1000000]
|
1155
|
+
['rbcrawl-fname=a.rb', 1000020]
|
1156
|
+
['rbcrawl-fname=a.rb', 1000028]
|
1157
|
+
['rbcrawl-fname=a.rb', 1000100]
|
1158
|
+
['rbcrawl-fname=b.rb', 1000005]
|
1159
|
+
['rbcrawl-fname=b.rb', 1000019]
|
1160
|
+
['rbcrawl-fname=b.rb', 1000111]
|
1161
|
+
||<
|
1162
|
+
|
1163
|
+
これなら、'rbcrawl-begin'をもつ最新のキーや、注目点直前の'rbcrawl-fname=a.rb'のキーなどが二分探索のコストで探せます。
|
1164
|
+
|
1165
|
+
Rindaの場合は強力なパターンマッチと引き換えに、Arrayを基本としたデータ構造を内部で使っていたため、データ量に比例して検索時間が増加する(O(N))という問題がありました。Dripの場合はRBTreeを使う事でtagやkeyの開始点まで比較的素早くブラウズが可能になっています。(O(log n))
|
1166
|
+
|
1167
|
+
このデータ構造のおかげで「消えないキュー」「いらなくなったら'rbcrawl-begin'でリセット」といった、一見富豪的なデータストレージが可能になっています。
|
1168
|
+
|
1169
|
+
|
1170
|
+
***まとめにかえて
|
1171
|
+
|
1172
|
+
この章の最後に、この小さな検索システムにERBの章で見せたようなWeb UIを追加してみましょう。この検索システムはクロウラとインデクサ、そしてミドルウェアのDripで構成されていました。ここにWEBrick::HTTPServerとサーブレットによるWeb UIを追加してみましょう。ERBの章ではWEBrick::CGIサーバを使って実験しました(覚えてますか?)。今回はHTTPServerを載せてみます。
|
1173
|
+
|
1174
|
+
こんなにたくさんのプロセスを起動するのは面倒ですよね。そこで、クロウラ、インデクサ、HTTPServer、Web UIを一つのプロセスに配置することにしましょう。dRubyを使って作ったシステムは、もともとプロセスの境界はRubyそっくりにできています。このためプロセス構成、オブジェクトの配置を変更するのは意外と簡単です。全部を一つに入れた完成版のスクリプトを以下に示します。
|
1175
|
+
|
1176
|
+
>|ruby|
|
1177
|
+
require 'index'
|
1178
|
+
require 'crawl'
|
1179
|
+
require 'webrick'
|
1180
|
+
require 'erb'
|
1181
|
+
|
1182
|
+
class DemoListView
|
1183
|
+
include ERB::Util
|
1184
|
+
extend ERB::DefMethod
|
1185
|
+
def_erb_method('to_html(word, list)', ERB.new(<<EOS))
|
1186
|
+
<html><head><title>Demo UI</title></head><body>
|
1187
|
+
<form method="post"><input type="text" name="w" value="<%=h word %>" /></form>
|
1188
|
+
<% if word %>
|
1189
|
+
<p>search: <%=h word %></p>
|
1190
|
+
<ul>
|
1191
|
+
<% list.each do |fname| %>
|
1192
|
+
<li><%=h fname%></li>
|
1193
|
+
<% end %>
|
1194
|
+
</ul>
|
1195
|
+
<% end %>
|
1196
|
+
</body></html>
|
1197
|
+
EOS
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
class DemoUIServlet < WEBrick::HTTPServlet::AbstractServlet
|
1201
|
+
def initialize(server, crawler, indexer, list_view)
|
1202
|
+
super(server)
|
1203
|
+
@crawler = crawler
|
1204
|
+
@indexer = indexer
|
1205
|
+
@list_view = list_view
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
def req_query(req, key)
|
1209
|
+
value ,= req.query[key]
|
1210
|
+
return nil unless value
|
1211
|
+
value.force_encoding('utf-8')
|
1212
|
+
value
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
def do_GET(req, res)
|
1216
|
+
word = req_query(req, 'w') || ''
|
1217
|
+
list = word.empty? ? [] : @indexer.dict.query(word)
|
1218
|
+
res['content-type'] = 'text/html; charset=utf-8'
|
1219
|
+
res.body = @list_view.to_html(word, list)
|
1220
|
+
end
|
1221
|
+
|
1222
|
+
alias do_POST do_GET
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
if __FILE__ == $0
|
1226
|
+
crawler = Crawler.new
|
1227
|
+
Thread.new do
|
1228
|
+
while true
|
1229
|
+
pp crawler.do_crawl
|
1230
|
+
sleep 60
|
1231
|
+
end
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
indexer = Indexer.new
|
1235
|
+
Thread.new do
|
1236
|
+
indexer.update_dict
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
server = WEBrick::HTTPServer.new({:Port => 10080,
|
1240
|
+
:BindAddress => '127.0.0.1'})
|
1241
|
+
server.mount('/', DemoUIServlet, crawler, indexer, DemoListView.new)
|
1242
|
+
trap('INT') { server.shutdown }
|
1243
|
+
server.start
|
1244
|
+
crawler.quit
|
1245
|
+
end
|
1246
|
+
||<
|
370
1247
|
|
371
|
-
|
1248
|
+
あたらしいクラスは二つです。一つはDemoUIServletで、Web UIを司ります。もう一つはDemoListViewクラス、CGIの見た目を生成するViewオブジェクトです。
|
1249
|
+
「if __FILE__ == $0」で囲まれたメイン部を見てみます。ここではcrawl.rbやindex.rbのメイン部で行っていたサブスレッドの生成のあと、HTTPサーバを起動しています。Ctrl-Cなどでシグナルを使ってサーバを終了させると、クロウラの仕事の合間に終了します。
|
372
1250
|
|
373
|
-
|
1251
|
+
クロウラとインデクサが一つプロセスでは、Dripの意味がないのではないか?という気がしなくもないですが、デスクトップのアプリケーションのように起動は簡単になりました。関連するプロセスが少ないのでデーモン化するのも楽です。ところで、プロセス間でオブジェクトの配置を変えるのは簡単でしたよね。このプロセス構成が気に入らなければ、クロウラとインデクサを分けるようなプロセス構成にすることも簡単です。
|
374
1252
|
|
375
|
-
|
1253
|
+
Dripの章の最後に、久しぶりにdRuby(プログラムリストには現れないけど、MyDripへのアクセスで使ってました)をERBを使って小さなシステムを組み立てました。dRuby、ERB、Rinda、Dripなどの私のライブラリは、あなたの手の中にある問題をあなた自身が解くのを支援できるように意図して作りました。どれも仕組みは単純でおもちゃみたいなライブラリですが、とても手軽にはじめることができます。
|
1254
|
+
本当に大きな問題、たとえばメインメモリにも一つのマシンのディスクにも入りきらないようなデータを扱ったり、無数のクライアントを本当に同時にハンドリングしたり、そういうのには向かないかもしれませんが、自分のPCや家庭のネットワークにあるようなあなたのデータをつかってミニチュアを書くのにはぴったりなツール群です。この本で紹介したライブラリやその考え方があなたのデザインのバリエーションを増やすことになれば、本当にうれしいことです。
|
376
1255
|
|
377
|
-
|
1256
|
+
おしまい
|
378
1257
|
|
379
|
-
(とちゅう)
|
380
1258
|
|