Lua
Smalltalk
Tarantool
SmalltalkDay 19

TarantalkでSmalltalkからDBを操る

More than 1 year has passed since last update.


はじめに

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オブジェクトが返ってみました。

ensureSpace.png

ensureSpaceNamed:により、'bookmarks'という名前のspaceがあれば取得され、なければ新規で作られます。

毎回存在チェックをしても良いのですが、面倒なのでTarantalkではこうしたensureXXXのメソッドをよく使います。


インデックスの作成

できたspaceに早速データを入れてみましょう。と言いたいところですが、その前にインデックスを作成します。

primaryIndex := space ensurePrimaryIndexSetting: [ :options | options tree; partsTypes: #(#string)].

primaryIndex.png

実はプライマリとなるインデックスがないとタプルを格納できません。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が返ります。

size.png


タプルを取り出す

取り出すにはインデックスに対し、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本家サイト'))

selectAll のコピー.png

検索キーを指定してみましょう。selectHaving:を使います。

primaryIndex selectHaving: #('Pharo').

先頭要素が'Pharo'に一致したタプルのみが返ります。

#(#('Pharo' 'http://pharo.org' 'Pharo本家サイト'))

selectHaving.png

今後は範囲を指定してみます。select:having:で比較用のオペレータとキーを指定します。

primaryIndex select: #>= having: #('Smalltalkユーザ会').

'Smalltalkユーザ会'以降のタプルが取得できました。

#(#('Smalltalkユーザ会' 'http://www.smalltalk-users.jp' 'Smalltalkユーザのためのハブサイト' '勉強会も毎月やっています') #('Tarantool' 'https://tarantool.org' 'Tarantool本家サイト'))

selectIteIteHaving.png

spaceを空にするにはtruncateを使います。

space truncate.

selectAllしても空の配列が返ってくるだけとなりました。なおspace dropとするとspaceそのものを削除できます。

truncatedSelect.png


タグ情報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').

smalltalkTags.png

タグ部分のみを取り出すにはcollect:してあげれば良いでしょう。

(primaryIndex selectHaving: #('Smalltalk')) collect: #last.

以下のようになりました。

#('cool' 'dynamic' 'object-oriented' 'pharo' 'pure')

smalltalkTags2.png

今度は'dynamic'タグのついた言語一覧を取り出してみます。secondaryIndexの方を使います。

secondaryIndex selectHaving: #('dynamic').

SmalltalkもLuaも動的です。

#(#('Smalltalk' 'dynamic') #('Lua' 'dynamic'))

dynLangs.png

動的言語の数を集計してみます。

secondaryIndex countHaving: #('dynamic').

2が返ってきますね。

countHaving.png


タグランキング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))

rankingSelect.png


まとめ

Tarantalkを永続化の目的で使ってみる例でした。Tarantoolは基本的にオンメモリにデータを保持しますが、write ahead log方式のため、突然プロセスが落ちたとしてもデータは復活するので安心です。

もう少し複雑な処理を行わせるには、Luaで関数を定義し、ストアドファンクションとしてTarantalkからcall:で起動するということをします。そうするとLuaの実行環境+DBMSとしてのTarantoolの力をフルに活用できることになるのですが、それはまたの機会ということで。