はじめに
Tarantalk紹介の続きです。今回はDBMSとしてのTarantoolを使ってみます。
この記事用にTarantalkを更新してしまったので、前回からの方は、以下を"do it"するようにしてください。最新になります。(get
しているところがポイントです。)
Metacello new
smalltalkhubUser: 'Pharo' project: 'MetaRepoForPharo50';
configuration: 'Tarantalk';
version: #stable;
get; load.
Tarantoolのデータモデル
TarantoolはいわゆるNoSQLに分類されるDBです。とはいえKVSほどフラットでも単純でもなく、いくぶん構造化されています。具体的には、タプルの集合をspaceという単位に分けた形で格納します。
タプルとは、Smalltalk的には単なる配列です。要素数はタプルごとに異なっていても構いませんし、要素の型が混在していても問題ありません。
ただし実際には検索のため、「n番目の要素は常に文字列」といった想定をして格納することが多いでしょう。Tarantoolでは、タプル中の任意の要素(の集合)に対し、HASH型やTREE型などの検索用インデックスを付与することができ、これにより高速な検索を実現しています。
より詳しくはこちらを参照してください。
https://tarantool.org/doc/book/box/data_model.html
spaceの作成
では論よりコードということで、spaceを作成しタプルを入れてみましょう。
前回と同じくコンソールからTarantoolを起動し、box.cfg{listen = 3301}
とします。
とりあえずWebブラウザのブックマーク情報でも入れてみることにします。'bookmarks'という名前のspaceを作ります。
tarantalk := TrTarantalk connect: 'taran:talk@localhost:3301'.
space := tarantalk ensureSpaceNamed: 'bookmarks'.
"Print it"するとspaceオブジェクトが返ってみました。
ensureSpaceNamed:
により、'bookmarks'という名前のspaceがあれば取得され、なければ新規で作られます。
毎回存在チェックをしても良いのですが、面倒なのでTarantalkではこうしたensureXXXのメソッドをよく使います。
インデックスの作成
できたspaceに早速データを入れてみましょう。と言いたいところですが、その前にインデックスを作成します。
primaryIndex := space ensurePrimaryIndexSetting: [ :options | options tree; partsTypes: #(#string)].
実はプライマリとなるインデックスがないとタプルを格納できません。spaceにensurePrimaryIndexSetting:
を送ります。
オプションをブロックの中で指定していますが、タプルの第1番目の要素の型を文字列にし、TREEのインデックスを作成するという意味です。
Tarantoolでは、インデックスの種類として、TREE、HASH、BITSET、RTREEを提供しています。よく使うのはTREEかHASHでしょう。何も指定しないとTREEになります。
要素の型はデフォルトではunsigned
です。今回は文字列を入れたいのでpartsTypes: #(#string)
としています。
タプルを入れてみる
では今度こそデータを入れてみましょう。insert:
を使います。
space insert: #('Tarantool' 'https://tarantool.org' 'Tarantool本家サイト').
space insert: #('Pharo books' 'http://files.pharo.org/books/' 'Pharoのオンライン書籍').
space insert: #('Pharo' 'http://pharo.org' 'Pharo本家サイト').
space insert: #('Smalltalkユーザ会' 'http://www.smalltalk-users.jp' 'Smalltalkユーザのためのハブサイト' '勉強会も毎月やっています').
'Smalltalkユーザ会'
だけ余計な情報が入っていて要素が一つ多いのですが、問題なく格納できています。
spaceのサイズを取得してみましょう。
space size.
"Print it"で4
が返ります。
タプルを取り出す
取り出すにはインデックスに対し、select系のメソッドを使います。
primaryIndex selectAll.
全件がこんな感じで返ってきます。アルファベット順に並んでいるのは、TREEのインデックスを使っているからです。
#(#('Pharo' 'http://pharo.org' 'Pharo本家サイト') #('Pharo books' 'http://files.pharo.org/books/' 'Pharoのオンライン書籍') #('Smalltalkユーザ会' 'http://www.smalltalk-users.jp' 'Smalltalkユーザのためのハブサイト' '勉強会も毎月やっています') #('Tarantool' 'https://tarantool.org' 'Tarantool本家サイト'))
検索キーを指定してみましょう。selectHaving:
を使います。
primaryIndex selectHaving: #('Pharo').
先頭要素が'Pharo'
に一致したタプルのみが返ります。
#(#('Pharo' 'http://pharo.org' 'Pharo本家サイト'))
今後は範囲を指定してみます。select:having:
で比較用のオペレータとキーを指定します。
primaryIndex select: #>= having: #('Smalltalkユーザ会').
'Smalltalkユーザ会'
以降のタプルが取得できました。
#(#('Smalltalkユーザ会' 'http://www.smalltalk-users.jp' 'Smalltalkユーザのためのハブサイト' '勉強会も毎月やっています') #('Tarantool' 'https://tarantool.org' 'Tarantool本家サイト'))
spaceを空にするにはtruncate
を使います。
space truncate.
selectAll
しても空の配列が返ってくるだけとなりました。なおspace drop
とするとspaceそのものを削除できます。
タグ情報DBを作ってみる
もう少し実用っぽい例として、タグ情報の管理を考えてみます。
| プログラミング言語 | タグ |
|-----------|------------|:------------:|
| Lua | dynamic |
| Lua | embeddable |
| Smalltalk | dynamic |
| Smalltalk | object-oriented |
| Smalltalk | ... |
のようなイメージで、任意のタグをプログラミング言語に付与でき、タグを集計できるようにします。
まず'tags'
という名前でspaceを新たに定義しましょう。
tagSpace := tarantalk ensureSpaceNamed: 'tags'.
次がインデックスの作成です。ここが工夫のしどころになります。今回は #('Smalltalk' 'object-oriented')
のようなタプルを重複して登録したくないので、プライマリインデックスをタプルの両要素に対象にして作成します。
primaryIndex := tagSpace ensurePrimaryIndexSetting: [:opts | opts partsTypes: #(#string #string)].
さらに、特定のタグが付与されている言語を検索したいので、タグ要素に対してセカンダリインデックスも用意します。
secondaryIndex := tagSpace ensureSecondaryIndexSetting: [:opts | opts isUnique: false; partsTypes: #(#string)].
'dynamic'
といったタグは、いろいろな言語に付与できなくては困りますので、isUnique: false
によりユニーク属性を解除しています。
では、データを投入してみましょう。
tagSpace insert: #('Smalltalk' 'pure').
tagSpace insert: #('Smalltalk' 'dynamic').
tagSpace insert: #('Smalltalk' 'object-oriented').
tagSpace insert: #('Smalltalk' 'pharo').
tagSpace insert: #('Smalltalk' 'cool').
tagSpace insert: #('Lua' 'table').
tagSpace insert: #('Lua' 'dynamic').
tagSpace insert: #('Lua' 'embeddable').
tagSpace insert: #('Lua' 'tarantool').
tagSpace insert: #('Lua' 'cool').
Smalltalkにどんなタグが付与されているかを見るには以下のようにします。
primaryIndex selectHaving: #('Smalltalk').
タグ部分のみを取り出すにはcollect:
してあげれば良いでしょう。
(primaryIndex selectHaving: #('Smalltalk')) collect: #last.
以下のようになりました。
#('cool' 'dynamic' 'object-oriented' 'pharo' 'pure')
今度は'dynamic'
タグのついた言語一覧を取り出してみます。secondaryIndex
の方を使います。
secondaryIndex selectHaving: #('dynamic').
SmalltalkもLuaも動的です。
#(#('Smalltalk' 'dynamic') #('Lua' 'dynamic'))
動的言語の数を集計してみます。
secondaryIndex countHaving: #('dynamic').
2
が返ってきますね。
タグランキングDBを作ってみる
タグクラウドのようなものを作るには、さらに各タグがどれだけ使われているかといった情報を保持する必要があります。
'tag_ranks'
という新たなspaceを作ってみましょう。以下のようなイメージです。
| タグ | スコア |
|-----------|------------|:------------:|
| pure | 30 |
| cool | 23 |
| ... | ... |
タグの名前順による並び替えなどは考えていないので、今回はHASHのプライマリインデックスにします。
tagRankSpace := tarantalk ensureSpaceNamed: 'tag_ranks'.
tagRankSpace ensurePrimaryHashIndex.
セカンダリインデックスの方は、スコアとして0から増えていく数を想定しているためunsigned
型にしました。
scoreIndex := tagRankSpace ensureSecondaryIndexSetting: [:opts | opts isUnique: false; partsTypes: #(#unsigned)].
適当にスコアを上げます。なお、ランキング集計というのはよくある処理なので、Tarantalkではat:plus:
という便利メソッドが用意してあります。
tagRankSpace at: 'pure' plus: 1.
tagRankSpace at: 'cool' plus: 1.
横着して、先ほどの'tags'のspaceの情報からタグを取り出してスコアに反映させることにします。
tags := (tagSpace primaryIndex selectAll) collect: #last.
tags do: [:each | tagRankSpace at: each plus: 1].
スコアが2以上の物を取り出してみましょう。
scoreIndex select: #>= having: #(2).
どうやらそれらしい結果が返ってきました。
#(#('pure' 2) #('dynamic' 2) #('cool' 3))
まとめ
Tarantalkを永続化の目的で使ってみる例でした。Tarantoolは基本的にオンメモリにデータを保持しますが、write ahead log方式のため、突然プロセスが落ちたとしてもデータは復活するので安心です。
もう少し複雑な処理を行わせるには、Luaで関数を定義し、ストアドファンクションとしてTarantalkからcall:
で起動するということをします。そうするとLuaの実行環境+DBMSとしてのTarantoolの力をフルに活用できることになるのですが、それはまたの機会ということで。