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 CHANGED
@@ -2,5 +2,3 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in Drip.gemspec
4
4
  gemspec
5
-
6
- gem 'rbtree'
@@ -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
- **TupleSpaceの永続化とその制約
3
+ この章では筆者が最近夢中になっているストリーム指向のストレージ、Dripについて紹介します。Dripはストレージであると同時に、プロセス間を協調のメカニズムでもあります。このくだりを聴いてもRindaとの共通部分が多くあると感じるでしょう。実際にRindaのアプリケーションを書いた経験を元にして書かれました。DripはRindaを置き換えるものではありません。どちらかというとオブジェクトの貯蔵庫であって、オブジェクト指向データベース、Key Value Storeやマルチディメンジョンのリストなど一連のストレージの習作を出発点としました。
4
4
 
5
- この節ではRinda::TupleSpaceをおさらいしながら、TupleSpaceに永続化したPTupleSpaceの概要を紹介し、その制約について考えます。
6
- PTupleSpaceはTupleSpaceのサブクラスです。タプルの状態の変化を逐次二次記憶にログして、次回の起動に備えます。PTupleSpaceを再起動すると最後の(最新の)タプルの状態のままに復元されます。
7
5
 
8
- タプルは実世界の「伝票」によく似ています。タプルをプロセス間でリレーしながら仕事を進めていく様子は、「伝票」を持ち回って仕事を行うのにそっくりです。Rindaの世界では「伝票」はTupleSpaceを介してプロセスからプロセスへ渡り歩きます。
6
+ **Dripとはなにか
9
7
 
10
- PTupleSpaceの提供する永続化は、TupleSpaceに蓄えられた伝票の束にのみ作用します。プロセスが持っている伝票をPTupleSpaceが知ることはできず、永続化されません。また、待合せている様子も永続化の対象ではありません。プロセスがある伝票を待っている、という状況までは再現できないのです。
8
+ Dripは追記型のストレージの一種で、Rubyオブジェクトを時系列にログします。Dripにはオブジェクトの追記のみ可能で、削除や更新はできません。dRubyのRMIを考慮した、局所的で安価なブラウズ用APIを用意してあります。オブジェクトのまとめ転送や、簡単なパターンによるフィルタ、シークをなど、です。
9
+ また、Dripはプロセス間の同期メカニズムでもあります。新しいオブジェクトの到着を待合せることができます。Dripでは一度保存されたオブジェクトは変化することはありません。複数のプロセスがばらばらの時刻に読み出した情報はどれも同じものですし、誰かが読んだオブジェクトを別の誰かが変更することはありません。この特性は分散ファイルシステムでよく見られる特性で、情報を排他的にアクセスしなくてはならない状況を減らすことができます。
11
10
 
12
- TupleSpaceに期待する機能が伝票の貯蔵庫であると考えた場合には、これで充分と言えるでしょう。PTupleSpaceにwriteした情報は再起動後もそのまま手に入ります。多くのアプリケーションではこれで間に合うかもしれません。ArrayやHashをそのままdRubyで公開する、あるいはログ付きで公開するのに比べて、TupleSpaceはどのくらい便利なのでしょうか。おそらく、RindaのTupleSpaceの強力なパターンマッチングにはある程度のアドバンテージがあるでしょう。そのパターンマッチングと引き換えに、あまり効率のよいデータ構造を使うことができませんでした。実装には線形探索が残っていて、要素数が増えたときに不安があります。
11
+ Dripはちょっとしたオブジェクトの保存先であり、プロセス間通信、バッチ処理のエーテルであり、ライフログです。単純な仕組みであるため、さまざまな用途への応用が考えられますし、それ故に使い途を想像するのが難しいとも言えます。私のDripを次のようなアプリケーションに使いました。
13
12
 
14
- TupleSpaceの本来の役割であるプロセス間の協調についてはどうでしょうか。PTupleSpaceに異常が起きてクラッシュしてしまった、再起動が必要になった、といった状況を想像してみましょう。まず、PTupleSpaceプロセスが停止することにより、readやtakeなどの待合せのRMIを実行していたプロセスではdRubyの例外があがります。PTupleSpaceが再起動されるとタプル群の最後の状態に復元されます。待合せをしていたプロセスは再起動したことを(知るのは難しいのですが)知ったのち、例外が発生した操作をやり直すことになります。しかし、そのように再開するスクリプトを書くのは難しく面倒です。
13
+ - バッチ処理のミドルウェア
14
+ - Wikiシステムのストレージと全文検索
15
+ - Twitterのタイムラインのアーカイブとbotフレームワーク
16
+ - irbでの作業中のメモ
15
17
 
16
- また、RMIのために抱え込む厄介な問題もあります。writeやtakeなど、タプルの状態を変える操作を考えてみましょう。通常のメソッド呼び出しでは処理が終われば呼び出した側に直ちに制御がもどりますが、RMIではサーバ側のメソッドの終了と、RMIの終了の間にソケット通信が行われます。つまり、処理が終わる前に例外が発生したのか、結果を伝える間に例外が発生したのか知ることができません。PTupleSpaceが二次記憶にタプルの操作をログしたあとに、クライアントにその完了が届く前にクラッシュしてしまう可能性があります。(全てがうまくいってからログする実装を選んでも、クライアントにタプルが届いたのち、ログするまえにクラッシュする可能性があります)
18
+ ちょっと雲をつかむような感じですね。次の節から、身近な同期メカニズムであるQueue、身近なオブジェクトの貯蔵庫であるHashとの違いをそれぞれ見ながら、Dripを紹介します。
17
19
 
18
- 異常終了といえば、プロセス側のクラッシュも考えられますね。PTupleSpaceの対象外ですがちょっと想像してみましょう。伝票をプロセスが取り出したままクラッシュしてしまうと、復元する方法がありません。次の短いスクリプトを見てみましょう。
20
+ **Queueとの比較
19
21
 
20
- >|ruby|
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
- PTupleSpaceTupleSpace自体の永続化を目的としたもので、それ自体はおそらく期待した通りに動作すると思います(そういうつもりで作ったので)。しかし、それだけでは協調するプロセス群をもとに戻すことはできません。ちょっとだまされた気分ですよね。
24
+ ここでのQueueとはRubyに付属のQueueクラスです。QueueFIFOのバッファで、要素は任意のオブジェクトです。Queueにオブジェクトを追加するのはpush、オブジェクトを取り出すのはpopです。popはオブジェクトを返すと同時に、Queueの中からそのオブジェクトを削除します。
25
+ 同時に複数のスレッドからpopすることも可能ですが、一つの要素は一つのスレッドにだけ届きます。同じ要素が複数のpopに届くことはありません。
26
+ 空のQueueに対してpopを行うとpopはブロックします。新しいオブジェクトが追加され、そしてそのオブジェクトを獲得したただ一人のスレッドに対してオブジェクトを届けます。
33
27
 
34
- **PTupleSpaceの使い方
28
+ Dripにおいてpopに相当する操作はreadです。readは指定したカーソルより新しい要素を返します。ただし、Dripの中から要素を削除することはありません。複数のスレッドが同じカーソルでreadした場合には、それぞれのスレッドに同じ要素を返します。
29
+ カーソルよりも新しい要素がない場合、readはブロックします。新しいオブジェクトがwriteされるとreadのブロックはとけて、新しい要素を返します。この場合も、複数のスレッドに同じ要素が届きます。
35
30
 
36
- skip
31
+ DripがQueueやRindaとよく似ているポイントは、要素の到着を待つことができるところです。
37
32
 
33
+ また異なるポイントは要素を消費するかどうかです。Queueのpopは要素を消費しますが、Dripのreadでは要素は減りません。これは何度でも/何人でも読めるということです。Rindaではアプリケーションのバグやクラッシュによるタプルの紛失はシステム全体のダウンを意味することがありますが、Dripでは要素の紛失を気にする必要はありません。
38
34
 
39
- **ストレージとしてのTupleSpace
35
+ 具体的なコードでDripのreadの様子を見ていきましょう。
40
36
 
41
- APIの視点からストレージとしてのTupleSpaceをおさらいします。
42
- TupleSpaceはタプル群を扱う集合構造です。同じ情報を複数持つことができるので、Bagと言えるでしょう。
43
- 最近の流行言葉にKVSという言葉ありますね。キーと値で表現するなら、同じキーを持つ要素の重複を許すストレージです。キーしかなくて、キーが値、にも見えますが。
37
+ ***ここで使用するメソッド
44
38
 
45
- これに対してHashは一つのキーに一つの値が関連付けられる辞書です。
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
- def [](key)
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
- 要素の増減がないケースでは前章で示した通り、グローバルなロックは不要です。だれかが更新中はその要素は取り出せませんが、更新が終わればまた書き戻されるはずです。ですから、単に要素が読めるまでreadで待ってしまえば良いことになり、局所的なロックとなります。
45
+ writeメソッドはDripの状態を変化させる唯一の操作で、要素を追加します。要素objをDripに格納し、格納されたキーを返します。objへのアクセスを容易にするために、複数のタグをしていできます。タグの使い方はあとで説明します。
78
46
 
79
- eachはどのように実装したらよいでしょう。TupleSpace全体を順に走査するうまい方法はありません。read_allで全ての要素のArrayを生成して、その配列にeachを委譲することになります。
47
+ もう一つのメソッドはreadです。
80
48
 
81
49
  >|ruby|
82
- def each(&blk)
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
- 分散ハッシュテーブルなどでもeachやkeysを低コストで実装するのは難しいかもしれません。
53
+ Dripをブラウズする基本となるメソッドがreadです。keyは注目点(カーソル)で、keyよりも後に追加された要素のキーと値の組をn個の配列で返します。要素がat_least個そろうまで、readはブロックします。timeoutを指定することができます。
54
+ 説明が長いですね。要するに「新しい要素をn返せ。at_least個揃うまでは待機せよ。」です。
92
55
 
93
- 流行のストレージには、常にキーでソートされているシーケンスを持つものがあります。並んでいることを利用して、大きな空間をブラウズするのが得意です。キーを工夫することでバージョン付きの情報を蓄えることもできます。RindaのTupleSpaceには、タプルを順序付けて並べることはできませんから、これを低コストで模倣するのは難しいです。
94
56
 
95
- ところであなたが欲しかった集合は本当にHashでしたか?
57
+ ***Dripのインストールと起動
96
58
 
97
- **ストリーム指向のストレージDrip
59
+ おっと。Dripのインストールを忘れていました。DripはRBTreeという赤黒木の外部ライブラリを使用します。gemを用意していただいたので次のようにインストールして下さい。
98
60
 
99
- この節で紹介するのは前章で説明したRD stream(消えないストリーム)のようなデータ構造を持つライブラリDripです。Dripはオブジェクトを書き込まれた順に蓄えるだけでなく、イベント通知メカニズムとしてプロセス間の同期にも使えます。Drip自体の機能は非常に小さいものですが、視点によっていろいろなものに見えます。ストレージにもバッチ処理のミドルウェアにも見えます。ひとことで説明するのは難しいですが、流行のサービスではTwitterのタイムラインがよく似ています。Rubyの標準ライブラリでたとえると、消えないQueue、消えないRindaです。たとえてもやっぱりよくわかりませんね。
100
-
101
- 簡単なスクリプトを使ってDripを使ってみましょう。
102
-
103
- ***Dripをインストールする
104
-
105
- (井上さんgems化してくださーい。)
61
+ >||
62
+ % gem ?????
63
+ ||<
106
64
 
107
- ***Dripを作る
65
+ 次にDripサーバを起動します。
108
66
 
109
- Dripは二次記憶としてプレーンなファイルを使います。Dripを生成するにはファイルを置くディレクトリを指定します。次のスクリプト(drip_s.rb)はDripを生成しdRubyでサービスするものです。
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の状態を変更する唯一のメソッドはwriteメソッドです。writeメソッドには覚えたいオブジェクトを指定します。ちょっと書いてみましょう。
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
- % irb -r drb --simple-prompt
146
- >> drip = DRbObject.new_with_uri('druby://localhost:54321')
147
- => #<Drip:...>
148
- >> drip.write('Hello, World.')
149
- => 1308221059579470
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
- ターミナル1で起動したDripサーバへの参照を作り、dripという変数に覚えます。次に'Hello, World.'という文字列をwriteします。writeの戻り値は書き込んだオブジェクトに対応するキーです。キーは単調増加する整数で、たいてい時刻から生成されます。時刻と最新のキーを比べて、最新のキーの方が大きい場合には+1したものをキーとします。多くの場合時刻と相互に変換できると思いますが、「ユーザが時計を設定してしまった問題」や時刻の分解能よりも細かい単位で書き込まれた場合などにはその限りではありません。いずれにしろ、このようなケースでもキーはいつも単調増加となります。
153
- キーがおおよその時刻に変換できるのは、人間があとでデータを調べるときに多少は便利です。Dripには同時にただ一つのオブジェクトだけが書き込めます。さまざまな事象は同時にいくつも発生しますが、Dripがそれを観測するのは同時にはただ一つです。事象が発生した時刻ではなく、Dripがそれを知った時刻と考えて下さい。
112
+ MyDripはこの固定のポートを指すDRbObjectですが、特別にinvokeメソッドが定義されています。MyDrip.invokeは新しいプロセスをforkし、必要であればDripデーモン起動します。すでに自分用のMyDripが動いている場合にはなにもせずに終了します。なお、MyDripを終了させるにはMyDrip.quitメソッドを使います。
154
113
 
155
- writeメソッドはStringに限らず、どんなオブジェクトでも保存できます。ただし、MarshalできないオブジェクトはDRbObjectで保存されますから、取り出して使えるようにするには多少の工夫が必要です。また、あとでオブジェクトを取り出すときのヒントとなる複数のタグを指定できます。タグはStringでなければいけません。
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
- >> drip.write({:text => 'Hello, World', :user => 'm_seki'}, 'greeting', 'test')
159
- => 1308221460421676
160
- >> drip.write(1308221460421676, 'test')
161
- => 1308221907348161
147
+ ターミナル3
148
+ % irb -r my_drip --simple-prompt
149
+ >> MyDrip.read(0, 1)
150
+ => [[1312541947966187, "Hello"]]
162
151
  ||<
163
152
 
164
- この操作では、Hashのオブジェクトに二つのタグ('greeting, 'test')を付けて書き込み、次に整数に'test'というタグを付けて書き込んでいます。writeの際に、一つのオブジェクトに複数のタグをつけることができます。タグはDripで一意なものでなく、同じタグを持つオブジェクトがすでにwriteされていても問題ありません。
153
+ readはカーソルからn個の要素を読むメソッドで、キーと値のペアの配列を返します。
154
+ 順に読むには次のようにカーソルを動かしながらreadすると良いでしょう。
165
155
 
166
- ***read
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
- Dripからデータを読む方法はたくさんありますが、基本となるのはreadです。readの引数は意外なほど多いです。
165
+ 二つ読めました。さらに読むとどうなるでしょう。
169
166
 
170
- >|ruby|
171
- read(key, n=1, at_least=1, timeout=nil)
167
+ >||
168
+ >> k, v = MyDrip.read(k, 1)[0]
172
169
  ||<
173
170
 
174
- 戻り値はキーとオブジェクト、タグからできたArrayのArrayです。読みたい要素数が1であってもArrayが返ります。keyよりも大きなキーを持つ要素をn個返します。at_leastは最低限返して欲しい要素数です。読める要素数がat_leastに達するまで、readはブロックします。timeoutはブロックの期限を指定します。最低でもtimeout秒は待ち、それを越えると例外があがります。nilを指定すると無限に待ちます。
171
+ kよりも新しい要素がないのでブロックします。ターミナル2から新しい要素を追加するとブロックがとけ、そのオブジェクトが読めるはずです。
175
172
 
176
- 先頭から一つずつ読み進めてみましょう。Dripのキーは正の整数ですから、先頭はキー0の次の要素です。0から二つの要素を読んでみましょう。
173
+ >||
174
+ ターミナル2
175
+ >> MyDrip.write('Hello, Again')
176
+ => 1312542657718320
177
+ ||<
177
178
 
178
179
  >||
179
- >> drip.read(0, 2)
180
- => [[1308221059579470, "Hello, World."], [1308221460421676, {:text=>"Hello, World", :user=>"m_seki"}, "greeting", "test"]]
180
+ >> k, v = MyDrip.read(k, 1)[0]
181
+ => [1312542657718320, "Hello, Again"]
181
182
  ||<
182
183
 
183
- 0の次のキーを持つ要素(つまり先頭、一番旧い要素)から二つ分の要素が集まってできたArrayが返りました。
184
+ どうですか?待合せできていますか?
184
185
 
185
- 次に先頭から順に一つずつ読んでみます。注目点のキーをkとして、kをずらしながらreadしていきましょう。
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, *tag = drip.read(k)[0]
191
- => [1308221059579470, "Hello, World."]
192
- >> k, v, *tag = drip.read(k)[0]
193
- => [1308221460421676, {:text=>"Hello, World", :user=>"m_seki"}, "greeting", "test"]
194
- >> k, v, *tag = drip.read(k)[0]
195
- => [1308221907348161, 1308221460421676, "test"]
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
- どうでしょうか?ひとつずつ順に取り出せている様子がわかりますか?Dripにおけるデータのブラウズは、キーをずらすことで表現します。readは与えられたキーのすぐ後の要素を返しますから、さっき読んだオブジェクトのキーを与えてreadすることで全ての要素を順に辿ることができます。C言語のstdioライブラリでいうとfseek()とfread()を一度に行うイメージですね。
201
+ 同じ要素が読めました。DripではQueueとちがって要素を消費しませんから、同じ情報をなんども読めます。その代わりにどの辺りの要素を読むのか、readのたびに指定しなくてはなりません。
200
202
 
201
- さて、4回目のreadでブロックしてしまいました。これは注目点のキーより新しい要素が存在しないからです。もう一つ端末を用意して、要素を追加してみましょう。
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
- % irb -r drb --simple-prompt
206
- >> drip = DRbObject.new_with_uri('druby://localhost:54321')
207
- => #<Drip:0x0000010086b130>
208
- >> drip.write('Hello, Again.', 'test')
209
- => 1308222915958300
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
- => [1308222915958300, "Hello, Again.", "test"]
274
+ ターミナル2
275
+ >> MyDrip.head(1, 'seki.age')
276
+ => [[1313358208178481, 29, "seki.age"]]
215
277
  ||<
216
278
 
217
- うん。ターミナル2のブロックは解け、新しい要素が届いたのがわかります。 
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
- ここでdrip_s.rbを停止させたらどうなるか実験してみましょう。まずターミナル2でreadを行ってブロックさせます。
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
- >> k, v, *tag = drip.read(k)[0]
308
+ ターミナル2
309
+ >> MyDrip.head(10, 'seki.age')
310
+ => [[1313358208178481, 29, "seki.age"], [1313358584380683, 49, "seki.age"]]
224
311
  ||<
225
312
 
226
- 続いてdrip_s.rbを[control]+Cなどで停止させます。
313
+ 先頭から10個の要素を要求しましたが、いまDripの中にある'seki.age'を持つ要素は二つだけなので、2要素のArrayが返りました。結果が複数返る場合、配列は旧い方から新しい方へ向けて並んでいます。
314
+
315
+ では存在しないキー(Hashでいうところのキー)を問い合わせるとどうなるでしょう。
227
316
 
228
- ターミナル1
229
317
  >||
230
- % ruby drip_s.rb
231
- ^Cdrip_s.rb:16:in `join': Interrupt
232
- from drip_s.rb:16:in `<main>'
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
- ターミナル2ではreadの最中にdrip_s.rbが終了したので例外があがります。待ち合わせしているときにサーバが終了すると、待合せは解除されることになります。
339
+ read_tagのブロックは解けて、いま追加したオブジェクトが返ります。
236
340
 
237
341
  >||
238
- DRb::DRbConnError: connection closed
239
- ....
342
+ ターミナル2
343
+ >> MyDrip.read_tag(0, 'sora_h.age')
344
+ => [[1313359385886937, 12, "sora_h.age"]]
240
345
  ||<
241
346
 
242
- 再びdrip_s.rbを起動してから、readを試してみましょう。
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
- % ruby drip_s.rb
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
- >> k, v, *tag = drip.read(k)[0]
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
- そしてターミナル3からもう一つ要素を追加します。ターミナル2のreadが完了し、その要素を読み出すことができるはずです。
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
- >> drip.write('drip drop')
259
- => 1308304037358423
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
- => [1308304037358423, "drip drop"]
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
- Dripは要素を二次記憶にwriteされたオブジェクトをログしており、次回の起動に備えています。これまでの実験の中でwriteされた5つの要素が本当に残っているかためしましょう。先頭から5つの要素をreadします。keyの最小値から5つの要素をreadするには、read(0, 5)とします。
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
- >> drip.read(0, 5)
272
- => [[1308221059579470, "Hello, World."], [1308221460421676, {:text=>"Hello, World", :user=>"m_seki"}, "greeting", "test"], [1308221907348161, 1308221460421676, "test"], [1308222915958300, "Hello, Again.", "test"], [1308304037358423, "drip drop"]]
544
+ >> k, = MyDrip.head(1, 'test1')[0]
545
+ => [1313573767321912, "sentinel", "test1"]
546
+ >> MyDrip[k]
547
+ => ["sentinel", "test1"]
273
548
  ||<
274
549
 
275
- drip_s.rbが一度終了しても内容が失われてないことが確認できます。
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
- **タグとその他のread系API
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
- writeの際につけたタグを使って、読み出す情報をフィルタすることができます。writeでは、一つの情報に複数のタグをつけることができ、read_tagはタグを一つ指定してそのタグを持つ要素だけを読み出すことができます。その他の引数はreadと同じです。
791
+
792
+ ***インデクサ
793
+
794
+ このインデクサは索引の作成、更新と、検索そのものも提供します。指定した単語を含んでいるファイルの名前を返します。このサンプルは実験用のミニチュアなので、インメモリに索引を作ることにしました。rbtreeが必要ですが、Dripが動いているならrbtreeはインストールされていると思います。
291
795
 
292
796
  >|ruby|
293
- read_tag(key, tag, n=1, at_least=1, timeout=nil)
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では全ての要素は一直線のストリームとして管理されます。要素はキーと値、複数のタグで構成されて、キーの順に並んでいます。readやread_tagは小さいキーから大きいキーの順にアクセスします。キーはwriteされた時刻を元に計算され、たいてい連続していません。readやread_tagの際に、存在しないキーを与えても問題ありません。そのキーよりも大きなキーを持つ、直近の要素からアクセスを開始します。readは与えたキーよりも大きなキーを持つ、直近の要素を返します。read_tagも同様に直近の要素を返しますが、タグがマッチしない要素はスキップされ、マッチした要素だけが返されます。タグを使うと一つのDripをタグで分類されたたくさんのDripのように見立てることもできます。
297
- また、readとread_tagのキーは共通ですから二つを組み合わせることもできます。例えば、read_tagで狙ったタグを持つ要素を取得して、それ以降の要素をreadで順に全て集める、といった操作です。
832
+ インデクサはDripから'rbcrawl'タグのついたオブジェクトを取り出し、その都度、索引を更新します。
298
833
 
299
- read_tagもreadと同様に要素が取り出せない場合に、ブロックして新しい要素の到着を待つことができます。
834
+ >|ruby|
835
+ @drip.read_tag(@cursor, 'rbcrawl', 10, 1)
836
+ ||<
300
837
 
301
- read系のAPIは他にも用意されています。
838
+ 第4引数の「1」に注目して下さい。先ほど「keyより新しい要素の数がat_least個に満たない場合は、新しいデータが追加されるまでブロックします」と説明したのを覚えていますか?一度に10個ずつ、最低でも1個ずつ返せ、という指示ですから返せる要素が一つもないときにはブロックします。
839
+ これによりクロウラが'rbcrawl'タグのデータを挿入するのをブロックしながら待ち合わせている事になります。
840
+
841
+ インデクサにとってrbcrawlタグのオブジェクトは更新イベントであると同時に文書でもあります。更新されたファイル名、更新時刻、内容がまとめて手に入ります。
842
+ また、DripはQueueとちがい、すでに読んだ要素を再び読むことが可能です。注目点の直前の要素を調べるolderなどで調べることが可能です。
302
843
 
303
844
  >|ruby|
304
- older(key, tag=nil)
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
- head(n=1, tag=nil)
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
- readやread_tagは過去から未来へ走査するAPIですが、olderとheadは過去方向への操作を補助するAPIです。
310
- olderはkeyで指定した要素の一つ旧い要素を返します。tagを使って要素をフィルタすることもできます。keyにnilを与えると最新を指定したことになります。older(nil)は最新の要素を一つ返します。
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
- headはolderを使ったコンビニエンスメソッドです。一番新しい要素までのn個を返します。tagを使って要素をフィルタすることも可能です。nが1の場合、一番新しい要素だけが入ったArrayを返します。nが2の場合には、一番新しい要素の一つ旧い要素と、一番新しい要素の入ったArrayを返します。Arrayの中は旧いから新しいものへと並んでいます。
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
- コンビニエンスメソッドとしてはolderの対になるnewerもあります。
881
+ 以下に完全なインデクサを載せます。
317
882
 
318
883
  >|ruby|
319
- newer(key, tag=nil)
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
- これは単なるread/read_tagのラッパーで、read(key, 1, 0)[0]あるいはread_tag(key, tag, 1, 0)[0]を簡単に呼べるようにしたものです。
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はPRb、KoyaなどのOODBへの挑戦(そして失敗)とRindaを実アプリケーションで使った経験、分散ファイルシステムへの憧れから書かれた小さなライブラリです。2009のRubyKaigiでの講演のテーマ、世界を並べるとも通じます。
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
- Dripはオブジェクトを保存することができます。オブジェクトは時系列整理されます。新しいオブジェクトを追加することはできますが、削除や変更はできません。KVSのようにキーと値を組にして覚えるのではなく、単に時系列に追加されます。KVSのキーとして、追加を受け付けた瞬間の時刻を使っている、と考えてもよいでしょう。
334
- Dripのキーは単調増加する整数です。この整数と時刻(Time)とは相互に変換することができます。オブジェクトを追加された時刻に従って順にブラウズすることができます。
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
- readメソッドにはいくつかバリエーションがあります。read_tagを使うとタグでフィルタしたストリームをブラウズできます。また、older, newer, headなどのread, read_tagをラップした簡易メソッドが用意されます。
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
- Dripではデータを保存するためにキーを考える必要がありません。Objectをnewするように単にwriteするだけです。(OODBのうれしさのひとつはオブジェクトの生成がnewでできることです。)
346
- オブジェクトを保存するとキーが返るので、それを使って思い出すことができますし、だいたいの時間がわかればキーを忘れてもなんとかなります。補助的な情報としてタグを使うこともできます。Dripではオブジェクトを変更したり削除したりすることはできませんから、他のプロセスやスレッドがオブジェクトを変えてしまう心配はありません。そうDripでは過去を変えることはできないのです。
1074
+ def query(word)
1075
+ @tree.bound([word, ''], [word + "\0", '']) {|k, v| yield(k[1])}
1076
+ end
1077
+ end
1078
+ ||<
347
1079
 
348
- KoyaやPRbの経験から「オブジェクトの生成」のためにユニークなキーを生成するのは私の中で大きなテーマでした。ここで紹介するDripではキーとして時刻を選びました。あるDripにとって、ある瞬間に同時に生成されるオブジェクトはありません。生成という処理を受け入れるのは同時にただ一つのスレッドだけです。
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
- Dripではあるキーよりも後に追加されたオブジェクトを取得することができます。カーソルをアプリケーション側が管理してすべての情報を順に辿れることを意味します。Rubyでよく使われるEnumerableとは違いますね。もしeachを実装するならアプリケーション側に書くとよいでしょう。この方式の大きなメリットは、ブラウズの中断や再開が容易な点です。
356
- 実運用でのバッチ処理アプリケーションなどでは、(とくに開発中など)処理が途中で失敗してしまったり、もっと効率のよいアルゴリズムを思いついてしまったりすることが珍しくありません。そんなとき、処理がうまく進んだ場所をメモしておいて、そこから再開することができます。また、失敗時に待ち行列から要素を紛失してしまうことも避けられます。
1093
+ 次のスクリプトは、lower_boundを使ったand検索のアルゴリズムを実験するものです。起動引数に与えたファイルの中から'def'と'initialize'が同時に出現する行を探します。文書の「位置」はこのケースでは「ファイル名」「行番号」を選びました。
357
1094
 
358
1095
  >|ruby|
359
- #典型的なDripのreadのループ
360
- def drip_each(drip, key=0)
361
- while true
362
- ary = drip.read(key, 10, 1)
363
- ary.each do |kv|
364
- yield(*kv)
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
- Dripの集合の先端(そのキーよりも新しいオブジェクトがまだ書かれてないところ)まで読み進めた場合、次のread操作はブロックされ、次のオブジェクトが届くまで休眠状態となります。この挙動はイベントの通知にDripを使うのに適しています。自分が知っているイベントよりも後に発生したイベントを読み尽したら、今度は新しいイベントが発生するまで休んでいることができます。
1248
+ あたらしいクラスは二つです。一つはDemoUIServletで、Web UIを司ります。もう一つはDemoListViewクラス、CGIの見た目を生成するViewオブジェクトです。
1249
+ 「if __FILE__ == $0」で囲まれたメイン部を見てみます。ここではcrawl.rbやindex.rbのメイン部で行っていたサブスレッドの生成のあと、HTTPサーバを起動しています。Ctrl-Cなどでシグナルを使ってサーバを終了させると、クロウラの仕事の合間に終了します。
372
1250
 
373
- RMIの性質の点から観察してみましょう。DripのアプリケーションはすべてDripへのメソッド呼び出しとその結果の取得という向きだけで構成されます。ブロック付きメソッド(イテレータなど)を使用していません。readとカーソルによるデータのブラウズ方法を思い出してください。ブラウズもイテレータは使わずメソッド呼び出しの繰り返しで行われます。このためDripからアプリケーションを呼び返す、アプリケーションの処理を待ってしまう状況はありません。典型的なオブザーバーパターンをナイーブに実現してしまうと、遅いアプリケーションに律速してしまいますが、DripのAPIではそのような状況に陥りにくいです。RMIの回数についても最適化されます。キーよりあとのN個のオブジェクトをまとめて取得することができるので、アプリケーションが仕事中に溜まってしまったオブジェクトを一度のRMIで読みだすことが可能です。
1251
+ クロウラとインデクサが一つプロセスでは、Dripの意味がないのではないか?という気がしなくもないですが、デスクトップのアプリケーションのように起動は簡単になりました。関連するプロセスが少ないのでデーモン化するのも楽です。ところで、プロセス間でオブジェクトの配置を変えるのは簡単でしたよね。このプロセス構成が気に入らなければ、クロウラとインデクサを分けるようなプロセス構成にすることも簡単です。
374
1252
 
375
- **世界をログから編み上げる
1253
+ Dripの章の最後に、久しぶりにdRuby(プログラムリストには現れないけど、MyDripへのアクセスで使ってました)をERBを使って小さなシステムを組み立てました。dRuby、ERB、Rinda、Dripなどの私のライブラリは、あなたの手の中にある問題をあなた自身が解くのを支援できるように意図して作りました。どれも仕組みは単純でおもちゃみたいなライブラリですが、とても手軽にはじめることができます。
1254
+ 本当に大きな問題、たとえばメインメモリにも一つのマシンのディスクにも入りきらないようなデータを扱ったり、無数のクライアントを本当に同時にハンドリングしたり、そういうのには向かないかもしれませんが、自分のPCや家庭のネットワークにあるようなあなたのデータをつかってミニチュアを書くのにはぴったりなツール群です。この本で紹介したライブラリやその考え方があなたのデザインのバリエーションを増やすことになれば、本当にうれしいことです。
376
1255
 
377
- 多くのリビジョン管理システムではすべての変更を保持していて、情報の追加・削除・更新は「変更ログ」への追記として表現されます。(Gitのようにオブジェクトのスナップショットを保持する作戦もありますが、この場合も情報は増える一方です。)Dripの提供する保存機能もこれらの変更ログと同じようなものと考えることができます。
1256
+ おしまい
378
1257
 
379
- (とちゅう)
380
1258