飲み屋に行くとかなりの確率で荷物を忘れて帰るmikioです。さて、今回はここ2ヶ月ほどで急ピッチで開発した軽量データベースライブラリ「Kyoto Cabinet」について紹介します。

開発の動機

以前から軽量データベースライブラリとしてご好評いただいているTokyo Cabinetですが、DBMとして必要十分な機能と性能を備えていてなかなか良いものだと自負しております。ただ、開発を進める中でいくつか不満な点があったのも事実です。端的に言えば、全てC言語で記述して、標準ライブラリ(とzlib/bzip2)以外の機能は全て自作しているので、最適化がしやすい反面、メンテナンスの難易度が高くなってしまっているというのが不満です。

そこで、多少性能が悪くなってもいいから、私自身としてお気楽に開発およびメンテナンスができて、移植性も高いような実装を作ってみようと思い立ったのが昨年10月頃。様々な検討を経てついにC++を採用し、STLを使って楽々にプログラミングしてみようということになりました。

特徴

KCの基本設計はTCのものを踏襲しているので、TCと同じく以下の点が優れています。

  • プロセス組み込みなので、ネットワーク通信等のオーバーヘッドがない。
  • 静的ハッシュの単純な構造なので高速で並列性も高い。
  • データベースファイルが小さく空間効率が高い。
  • オンメモリデータベースとファイルデータベースを使い分けられる。
  • トランザクションによってACID属性を確保。
  • 動的デフラグでデータベースの肥大化を抑止。
  • APIが単純で使いやすい。
  • 名前がかっこいい。

一方で、KCは実装上の工夫をさらに重ねて、TCに比べて以下の利点を獲得しています。

  • さらに空間効率が高い。
  • さらに並列性が高い。
  • 下層の抽象化による非POSIX環境への対応。
  • レコード操作の究極的な抽象化。
  • 実装上の細かい改善。

KCの性能はTCに比べてかなり低く、現状では半分以下です。しかし、ユーザランドでの並列性は高いはずなので、OSやハードウェアの並列性が上がれば、並列処理で使った場合の全体のスループットがTCに勝つ日も来るかもしれません。

空間効率が高い

いかなるデータベースにおいても、レコードを管理するためのデータを記憶しておく必要があり、TCやKCではレコードに前置するヘッダとしてそれを表現しています。データベースの空間効率を高めるにはこのヘッダをできるだけ小さくすることが重要です。

TCでは通常モードで14バイト、ラージモードで22バイトのヘッダ領域をレコード毎に必要としていました。通常モードとラージモードの違いはレコードのアドレッシングのために4バイトの領域を使うか8バイトの領域を使うかの違いです。4バイトだと2^32=4GBまでの値を表せ、それにデフォルトアラインメントの16バイトを掛けた64GBまでのデータベースを扱えることになります。8バイトだと2^63=8EBまでのデータベースを扱えます。

今日の一般的な計算機環境を考えると、4バイトアドレッシングは非力すぎる一方、8EBのストレージなんてものも存在していません。そこで、KCではモードを統一して、6バイトアドレッシングを採用しています。6バイトだと2^48=256TBまでの値が扱え、デフォルトアラインメントは8なので、2PBまでのデータベースを扱えることになります。1台のデータベースで扱えるサイズとしては当面はこれで十分でしょう。なお、アラインメントは32768まで上げることができるので、ライブラリとしての最大データベースサイズはTCと同じく8EBまでとなります。

アドレッシングの幅が4バイトから6バイトに上がった分だけ空間効率が悪化するので、それが最小限になるように努力もしています。レコードを識別するためのマジックナンバをサイズ管理用の領域に押し込んだり、後述の二分探索木をバランスさせるためのキーを動的に再計算することで記憶領域をとらないようにしたりして、16バイトまで抑えました。TCの通常モード14バイトに比べると多少増えていますが、ラージモードの22バイトに比べるとかなり減っています。

TCやKCでは静的ハッシュ法によるインデックス(バケット配列)を管理することでレコードの探索を高速化していますが、バケット配列の要素が足りない場合でも著しく遅くならないようにするために、ハッシュ値の衝突管理を二分探索木で行って計算量を抑制しています。とはいえ、単一のマシンでどれだけのレコードを管理できるかはあらかじめ予測できるはずなので、きちんとチューニングすれば二分探索木の機構は不要とも言えます。そこで、きちんとチューニングできる人達のために、「線形モード」も用意しました。二分探索でなく線形リストとして衝突管理を行うことで子供のアドレスの6バイトを節約するのです。つまり、線形モードを使えば、バケット配列のサイズに対してセンシティブな性能特性にはなるけれど、ヘッダのサイズを10バイトにまで減らせるのです。

さらに、KCは使うけどデータベースサイズが32GB以下だと確実に分かっている場合には4バイトのアドレッシングで十分なので、「スモールモード」も用意しました。線形モードとスモールモードを併用するとレコード毎のヘッダは8バイトで済みます。ここまでやればTCに対して優位性を示せるでしょう。

並列性が高い

TCやKCではPthread(POSIXスレッド)パッケージでマルチスレッドの排他制御を行っています。例えばデータベースファイルのサイズというメタデータを更新するためには、mutexでロックをかけて、更新を行って、ロックを解除するという手順を踏んでいます。この構造自体は仕方ないことですが、変数一つを更新するためにmutexのロック/アンロックのオーバーヘッドがかかるのは非効率なので、できればもっと軽量な方法を使いたいところです。

うまいことに、GCCのバージョン4からはi386のアトミック演算機能を呼べる拡張機能がサポートされているので、それを使うことにしました。もちろんIntel系以外のCPUを使った環境やGCC以外のコンパイラでビルドする場合にはそれだとうまくいかないので、その場合はspinlockで該当の変数を保護する代替実装を適用するようにしています。

それ以外にも、並列性に着目してコード全体を見直して、ロックフリーとはいかないまでも、クリティカルセクション内でブロックし得るシステムコールを一切呼ばないというところまでは来ています。

TCと同じく、「pread/pwriteというシステムコールは明示的な内部状態(ファイル位置)を持たないので並列性が高いと言われつつも実際には期待外れな問題」に対処するためにmmapを積極活用しています。KCでは全てのファイルI/Oをmmap経由でやろうとも思ったのですが、諸事情で断念して、結局mmapとpread/pwriteを併用する実装になっています。

非POSIX環境への対応

TCでは最適化のためにモジュール化の原則を崩しまくっていわばモノリシックな構造になっているので、POSIX依存のコードをそうでないOS用に書き換えるのが絶望的に難くなっています。とはいえ、TCは主にWebサービスのバックエンドとして利用することを想定しているので、LinuxやFreeBSDやSolarisで動けば十分だという割り切りの上で設計をしていたので、これは問題ではありませんでした。いわゆる組み込み系やWindows等のデスクトップ環境で動く必要はないということです。

一方、KCは組み込み系やWindowsでも動くようにする予定です。よって、多少オーバーヘッドがあろうとも、処理系依存部分は徹底的にモジュール化して、C++03標準で定義されていないシンボルはハッシュDB本体の実装には一切現れないようにしました。主にファイルI/Oを抽象化するFileクラスとスレッド管理を抽象化Threadクラスがそれを担っています。

Windows版はまだ開発に着手できていませんが、数ヶ月以内にはリリースしたいと思っています。構造を単純化したのでPure Java版も作れるだろうとかDBフォーマットをRFCにできたら格好いいとか妄想は膨らみますが、多分口だけで終わると思います。

レコード操作の抽象化

KCを設計する際に、「KVSとは何だろう」「DBMとは何だろう」とかいう哲学的な部分を考察しました。その結果、KVSとは以下のメソッドを宣言したインターフェイスであるという仮説が導き出されました。

  • set : キーと値の組を記憶し、後でgetできるようにする。
  • get : キーを指定して、そのキーを伴って直前に記憶された組の値を取得する。
  • remove : キーを指定して、そのキーを伴って直前に記憶された組の記憶から消す。
  • open : 直前にcloseした際の記憶を復元する。
  • close : setやremoveによって更新された論理構造を記憶装置に保存する。

KVSの「Key-Value」はset/get/removeを示し、「Store/Storage」はopen/closeを示す感じでしょうか。そして、DBMとは、上記KVSの実装形態の一つであり、実装として以下の条件を満たすものです。

  • ファイル記憶 : 記憶状態をファイルシステム上のファイルに保存する。
  • プロセス組み込み : ネットワーク通信を伴わず、関数呼び出しだけの低いオーバーヘッドでメソッドを呼び出せる。

これらはあくまでオレオレ定義にすぎませんが、KCはDBMであり、DBMはKVSなので、KCはKVSだと考えています。まあ、「分散しないものはKVSとは言えない」とか、「ACIDじゃないものはそもそもDBとは言えない」とかいうオレオレ定義をするのも自由なので、TCやKCを実際のところ何と呼ぶのかはユーザの皆様にお任せすることにします。

話を戻して、KVSインターフェイスの一実装であると認識されるKCですが、並列処理を想定した場合、キーと値の組に対する操作はset/get/removeだけでは済まないというのが課題になります。単一スレッドの直列処理の場合、レコードの状態をgetしてから、その内容やそれ以外の状況に応じてそのレコードをsetしたりremoveしたりすれば、キーと値の組に対するいかなる操作も実現することができます。一方で、並列処理の場合、getしてからset/removeするまでの間に別のスレッドがレコードの状態を変更してしまう可能性があるため、何らかの排他制御機能が必要となります。そのためには主に以下の二つの戦略があります。

  • lock/unlockメソッドの定義 : 該当のレコードをlockしてから、任意の操作を行って、最後にunlockするのをアプリケーションの責任で行う。
  • 各種の操作を全てライブラリ側で定義 : 上記の操作のひとつひとつをライブラリ側で実装する。

前者はライブラリ側の実装が簡素になるとともに、アプリ側で排他制御の有無や粒度を自由に設定できるという利点がある反面、実装が複雑になり、デッドロック等の酷いバグを生み出す原因になります。後者逆の特性で、アプリ側は不自由ながらも簡潔になる反面、ライブラリ側のメンテナンスは非常に大変になります。TCでは後者を採用して、putcatとかaddintとかaddbouleとか多数のメソッドを実装しまくるという苦労を味わっていますが、KCではその苦労を少しでも軽減したいものです。

ここでKVSの定義を再検討してみましょう。レコードに対するいかなる操作も、「特定のレコードの状態を調べて、それに応じて更新操作をしたりしなかったりする、という挙動をアトミックに行う」という風に抽象化できます。例えば、各種操作は以下のような言い換えが可能です。

  • set : レコードの状態を調べるが、その状態の如何に関わらず新しい値に書き換えて更新する。
  • get : レコードの状態を調べて、レコードがあれば値を返し、元の状態をそのまま残す。
  • remove : レコードの状態を調べて、レコードがあれば削除して、なければ何もしない。
  • increment : レコードの状態を調べて、レコードがあればそれを数値とみなして、なければ0とみなして、付与の値を足した値をもって、レコードの値を書き換える。
  • append : レコードの状態を調べて、レコードがあればその値に、なければ空文字列に、付与の値を後置した値をもって、レコードの値を書き換える。

例えて言うなら、キーに対応するレコードの空間にアプリケーションの実行者を案内して、その値の読み書きを自由にさせるという操作を、その空間に限定してアトミックに行わせるということです。すなわち、KVSの仕様としては、操作の具体的な内容ではなく、アトミック性を保証する範囲が重要であるということになります。その考えに基づいてKVSを定義すると、以下のようになります。

  • キーに対応する値の更新履歴を1世代以上記憶することができる。
    • openメソッドは記憶のある時点のスナップショットを復元する
    • closeメソッドは記憶のその時点のスナップショットを保存する
  • キーに対応するレコードの状態の取得と更新をアトミックに行える
    • 付与のキーに基づくレコードがある場合、そのレコードの値を渡してコールバック関数を呼ぶ
    • 付与のキーに基づくレコードがない場合、何も渡さずコールバック関数を呼ぶ
    • 上記のコールバック関数が値を返したなら、その値を元のキーに対応する値として記憶する

具体的なAPI

ここまでの議論で新たに認識したKVSインターフェイスをC++のAPIに落とし込んでみましょう。

class DB {
  // レコードの空間を訪問する操作主体のクラス
  class Visitor {
  public:
    // 訪問時、更新が必要ない(no operation)の場合に返す値
    static const char* const NOP;
    // 訪問時、そのレコードを削除したい場合に返す値
    static const char* const REMOVE;
    // 仮想デストラクタ
    virtual ~Visitor() {}
    // 既存のレコードを訪問した場合に呼ばれるコールバック関数
    virtual const char* visit_full(const char* kbuf, size_t ksiz,
                                   const char* vbuf, size_t vsiz, size_t* sp) = 0;
    // 存在しないレコードを訪問した場合に呼ばれるコールバック関数
    virtual const char* visit_empty(const char* kbuf, size_t ksiz, size_t* sp) = 0;
  };
  // 訪問者を受け入れる
  bool accept(const char* kbuf, size_t ksiz, Visitor* visitor, bool writable);
  // データベースを開く
  bool open(std::string& path, int mode);
  // データベースを閉じる
  bool close();
};

上記はKyoto Cabinetの実際のインターフェイスです。アプリケーションは、openでデータベースオブジェクトを利用可能にした後、DB::Visitorを実装した任意のクラスを定義して、それをデータベースオブジェクトのacceptメソッドに渡して操作を行い、最後にデータベースオブジェクトを閉じます。データベースから「hoge」というキーに対応する値を検索して表示するアプリケーションは以下のようになります。

DB db;
db.open("casket", DB::OREADER);
class : public DB::Visitor {
  virtual const char* visit_full(const char* kbuf, size_t ksiz,
                                 const char* vbuf, size_t vsiz, size_t* sp) {
    cout << string(kbuf, ksiz) << " has " string(vbuf, vsiz) << endl;
    return NOP;
  }
  virtual const char* visit_empty(const char* kbuf, size_t ksiz) {
    cout << string(kbuf, ksiz) << " is empty" << endl;
    return NOP;
  }
} visitor;
db.accept("hoge", 4, &visitor, false);
db.close();

毎回Visitorを実装するのは面倒なので、KCの実際のDBクラスには典型的な操作であるset/get/remove/append/incrementをacceptを使って実装したものがビルトインとして組み込まれています。ただ、レコードの操作は必ずacceptを経由するようにしているので、ライブラリ内の実装はとても簡潔で保守しやすくなっています。

詳細についてはKCのチュートリアルAPI文書をご覧ください。

実装上の細かい改善

KCではハッシュ関数としてMurMur hashを採用しています。TCで採用していた各バイトを37倍して足すハッシュ関数だと、自然言語の単語のようなものをキーにする際にはとても効率がよいのですが、intなどのバイナリ表現をそのままキーにすると衝突率が高まるという弱点があります。TCでは主なユースケースのひとつとしては全文検索の転置インデックスを想定していたのでそれでもよかったのですが、KCではより汎用性を重視して、数値のバイナリでも偏りが出にくいという評判のあるMurMurを採用した次第です。MurMur hashと同じような特性を示すFNV hashも捨てがたいところで、自然言語の単語ではFNVの方がよさげでしたが、やはり数値バイナリでの安定性が上のような気がするMurMurを採用しました。とはいえ、ハッシュ関数はプラグインで入れ替えられるようにする予定です。

TCでは値の圧縮にgzipとbzip2を切り替えて使えるようにしていましたが、bzip2モードを使う人があまり多くない割に、libbz2-devパッケージをインストールしていなくてはまる人が多いので、bzip2は廃止しました。ただし、TCと同じく任意の圧縮関数をプラグインで入れ替えられるようにしているので、bzip2はもちろん、LZOやLZSSやLZMAを使うことも可能です。

特定のハッシュ関数や圧縮関数で作ったデータベースファイルは同じ関数のセットを組み込んだDBオブジェクトで開かないと不整合が起きるわけですが、チェックサム機構を備えることでそういった不整合を事前に検出できるようにもしています。

TCでは全レコードを横断的に参照するために内部イテレータ方式を採用していました。内部イテレータ方式は、データベースの内部にどこまで読んだかという情報を保持するので、アトミック性の確保が容易になり、実装も単純になる反面、同時に一つのイテレータしか使えないという弱点があります。KCでは内部イテレータと外部イテレータの両方をサポートしています。外部イテレータ方式は、どこまで読んだかという情報を保持するカーソルオブジェクトを生成してアプリケーション側で管理するものです。アトミックに全レコードを操作したい場合には内部イテレータを用いて、アトミック性はいらないから複数スレッドで並列にスキャンを行いたい場合には外部イテレータを使うとよいでしょう。

さらに、KCではイテレータによるレコードアクセスもacceptメソッドを経由して行うので、Visitorによる任意のアトミック処理ができ、そしてイテレーション中のレコードの書き換えも簡単に行うことができます。どうせならSTLのstd::map::iteretorと互換にしようともちらっと思いましたが、あまりに面倒なので挫折しました。

まとめ

Kyoto CabinetはTokyo Cabinetの後継の製品で、性能は悪化していますが、機能性と並列性と移植性と保守性は向上しています。まだまだアルファバージョンで、実用に耐える品質とは言い難い状態ですが、そこそこ使える状態まではもっていく所存です。

TCは不要なのかというのがFAQになりそうなのでここで答えておくと、そんなことはありません。おそらく、TCが使える環境ではTCを使い続けるのがよいでしょう。POSIX準拠でない環境(主にWindows)でDBMが欲しい場合にはKCが役立つと思います。そういう意味では、KCはTCの後継というより、QDBMを共通の親とするTCの兄弟と言った方が適切かもしれません。

ドラクエは卒業して、もっと英語漬けをやっているmikioです。さて今回は、データベースサーバTokyo Tyrantとテーブルデータベースを使ってリアルタイム検索システムを構築する方法について語ります。

テーブルDBを分散させたい

Tokyo TyrantでもテーブルDBがサポートされているわけですが、これはリアルタイム検索システムへの布石です。テーブルDBは任意のコラムにインデックスを張ることができ、時系列のコラムにインデックスを張ればその値によって古いコラムを効率的に消すことができます。チュートリアルの「Persistent but Expirable Cache」でもその方法を示しています。また、任意のコラムに分かち書きトークン方式もしくは文字N-gram方式で転置インデックスを張ることができます。これらを総合すると、最新のデータのみを保持してサイズと性能を一定に保ったインデックスを維持できることになります。

上記で最新のデータを検索できるわけですが、次にそれをスケールアウトできるようにしたくなります。「リアルタイム検索システム」として運用しようとするならば、更新の負荷をできるだけ小さくするために、インデックスを小さな規模で分割しておくことが望ましいでしょう。更新の負荷、とりわけ登録文書を削除する処理の負荷が重いのです。Posting Listから該当の文書IDの要素を消すために全体をスキャンしなきゃなりません。

ところで、TCやTDでの転置インデックスは必ずPosting Listを同一のページに入れる構造になっていて、でかいPosting Listを格納しようとした時に効率が悪いというご意見をいただくことがよくあります。たしかに、でかいPosting Listはチェーンでつないで別ページで管理する方が文書の登録は速くて済むでしょう。しかし、それだと検索や削除は必ずしも速くならないのです。私の見解としては、Posting Listが長くてやってられなくなった時点でインデックスの分割を上位レイヤで検討すべきということです。遥か昔にSnatcherという全文検索システムを作っていた時はチェーン方式を採用していたのですが、その時に懲りました。

並列メタ検索メソッド

分割されたデータベースへの文書の登録は、IDのハッシュ値でいわゆるアルゴリズム方式の水平分散で行えばよいでしょう。削除も同様です。問題は検索操作です。全てのデータベースに対して個別に検索を行って、その結果をマージする必要があります。ちと大げさですが、これを「メタ検索」と呼びます。ですが、メタ検索はソート条件などを維持する必要があって実装が面倒なので、アプリ側の工夫だけで頑張れというのは酷ですよね。なので、メタ検索用のユーティリティを用意しました。以下のシグネチャです。

/* Search records for multiple servers in parallel.
   `qrys' specifies an array of the query objects.
   `num' specifies the number of elements of the array.
   The return value is a list object of zero separated columns
   of the corresponding records. */

TCLIST *tcrdbparasearch(RDBQRY **qrys, int num);

それぞれ別のRDBオブジェクトから作ったクエリオブジェクトを格納した配列とその要素数を与えると、並列してクエリを投げて、その該当のレコードを取得して順序を整えてから、結果セットをリストとして返します。結果セットはキーだけでなくレコード本体も取得してきているので、1回のネットワーク通信で検索結果を提示できます。処理は内部でスレッドを立てて勝手に並列化してくれます。

利用例を以下に示します。この関数は、データベースオブジェクトの配列rdbsとその要素数divnumと検索式exprを渡すとメタ検索を行ってくれます。

static void searchprint(TCRDB **rdbs, int divnum, const char *expr){

  // クエリオブジェクトの準備
  RDBQRY *qrys[divnum];
  for(int i = 0; i < divnum; i++){
    qrys[i] = tcrdbqrynew(rdbs[i]);
    // textコラムの全文検索
    tcrdbqryaddcond(qrys[i], "text", RDBQCFTSEX, expr);
    // numコラムの降順に整列
    tcrdbqrysetorder(qrys[i], "num", RDBQONUMDESC);
  }

  // 一撃で並列検索
  TCLIST *res = tcrdbparasearch(qrys, divnum);
  int rnum = tclistnum(res);

  // 結果セットの個々の要素をループ
  for(int i = 0; i < rnum; i++){
    // コラムオブジェクトを結果セットから抜き出す
    TCMAP *cols = tcrdbqryrescols(res, i);
    // 各コラムを印字する
    tcmapiterinit(cols);
    const char *name, *value;
    while((name = tcmapiternext2(cols)) != NULL){
      value = tcmapget2(cols, name);
      printf(" [%s=%s]", name, value);
    }
    printf("\n");
    tcmapdel(cols);
  }

  // 片付け
  tclistdel(res);
  for(int i = 0; i < divnum; i++){
    tcrdbqrydel(qrys[i]);
  }
}

皮算用

最近のPCはコアが多いから、各マシンに4つくらいTTを立ててもいいでしょう。仮にそのマシンを4台セットで利用するなら、16個のTTサーバが動くことになります。1サーバに10万件のデータを入れるならば、総計160万件のデータを検索対象にすることができます。これはmixiの1日の日記を全部格納しても余裕の数です。耐障害性を考えて二重化しても8台で済みます。もちろん、実際のインデックス分割数とマシン台数は検索と更新の負荷に応じて決定すべきです。

1日分しか検索対象にできないなんて意味がないと言われるかもしれません。でも、リアルタイム検索はそれでいいのです。24時間も稼げれば、その間に別の大規模なインデックスを構築できます。古いインデックスとリアルタイムインデックスのメタ検索を行えば全コンテンツをリアルタイム検索していることになるというわけです。

まとめ

Tokyo Tyrant上でテーブルDBを動かせばリアルタイム検索ができます。そのためにはインデックスを小さく分割して分散させます。分散したサーバを対象とした検索操作はメタ検索メソッドを用いれば簡単です。リアルタイム検索は最新のものだけを扱い、全体のインデックスと相互補完する目的で用います。

このアーキテクチャでどこまでスケールするのか、あるいはどこまで性能が出るのかは、まだ実データで実験していないので確たることは言えません。いずれちゃんとした性能測定をしてまた報告したいと存じます。TT+テーブルDBを再利用するのではなくリアルタイム検索専用のインデックスを書き起こした方がもちろん高効率が実現できるのでしょうし、あるいはインデックスを作らないでオンメモリのキャッシュを逐次探索するモデルの方が適する場合もあるでしょう。とはいえ、テーブルDBを並べるだけという手軽さの点では今回の手法も利点があるかなと思っています。

ドラクエのプレー時間がついに150時間を突破して妻の視線が痛いmikioです。今回は、かんたんCMS「Tokyo Promenade」にスクリプト言語Luaを組み込んでカスタマイズする方法について述べます。

なぜスクリプト言語処理系を組み込むのか

Tokyo Promenade(TP)はCで書かれていて軽量で高速に実行できるCMSです。PerlやRubyなどのスクリプト言語で書かれたCMSはそのソース自体を編集して改良するのが容易ですが、Cの場合は再コンパイル作業が必要だし下手に手を出すとメモリ破壊などの致命的なバグを入れてしまう可能性が比較的高いので、ソース自体を編集してカスタマイズを行うのは現実的ではありません。

そこで、TPではプレゼンテーション層の機能をできるだけテンプレート側に委譲させるとともに、さらに装飾の多くはCSSを編集するだけで変更できるように配慮しています。テンプレートにはJavaScriptを埋め込むこともできますから、入力パラメータに対して決定的な(つまり入力パラメータとTPのデータベースだけに依存してそれ以外の外部要因に依存しない)情報を表現するのであれば、そのカスタマイズ性は現状で十分だと言えます。

HTMLとCSSとJavaScriptをいじればほとんどのカスタマイズはできるのに、なぜサーバ側でのスクリプティングが必要なのでしょうか。主にJavaScriptの制限に起因する2つの理由があります。ひとつは、クライアント側のJavaScriptだけでは計算結果の保存をクッキーに頼ることになるので、サイズの大きいデータを保存したり、ユーザ間で共有したりするのが難しいということです。もう一つは、クライアント側のJavaScriptだけでは外部のサイト(クロスドメイン)のデータを通信で引っ張ってくることができないということです。

最も単純な例としてアクセスカウンタが挙げられます。これはページが閲覧される度に数値をカウントアップするだけの機能ですが、その値を全ユーザで共有する必要があるので、クライアント側の努力だけで実装するのは困難です。ページが閲覧される度に起動されるプロシージャをサーバ側に仕掛けることができればこの問題は簡単に解決します。

実用的な例だと、記事内にあるアンカーテキストのリンク先で書籍のISBNが書かれている場合に、その書籍のタイトルや表紙画像をAmazonから取得して表示することが考えられます。このように、リソース識別子に関連付けられたメタデータを外部サイトから取得して記事に埋め込んで表示したいというニーズは各種考えられますが、セキュリティ上の理由でクロスドメインの通信を禁じられているクライアント側の努力だけではそれに答えることはできません。

アクセシビリティ重要

Amazonの書籍情報を表示したいだけならば、Amazonが提供する埋め込み用のHTMLスニペットを記事に張り付ければいいと人は言うかもしれません。しかし、それはTPの思想からは外れます。TPの記事の素データはWiki記法で文書の論理構造を記述したものです。デフォルトでたまたまHTMLに変換して表示することができるようになっているだけで、HTMLは表現方法の一形態にすぎません。LaTeXやDocBookやroffを介してプレゼンテーションを行うUAへの対応を今後追加するかもしれませんし、そのような対応を容易にしておくことがTPのアクセシビリティのポリシーなのです。その観点に立つと、Wiki記法にHTMLを混ぜてしまったらHTMLとしてしか表現できないようになってしまうので、許容できません。

URIの普遍性という観点も忘れてはなりません。書籍紹介をする際にAmazonのURLを記事に埋め込むことがよくありますが、Wiki記法のレベルでそれをやるのは望ましくありません。その代わりに、ISBNなどの、より普遍性の高いものをリンク先のURIとして記述して、表示時にAmazonのURLに変換する方がアクセシビリティ(アベイラビリティ)が高いと言えます。仮にAmazonが潰れたり買収されたとしてもISBNは不変なので、表示スクリプトを改修してbk1や紀伊國屋にある同等の書籍紹介ページへ誘導することができるからです。

TPは記事の作者の思考を直列化するための機構です。したがって、言及の意図が「Amazonの書籍紹介ページ」に向けられているのであればAmazonのURLを使うべきですが、そうでなく「書籍の内容」に向けられているのであれば、ISBNなどを使うべきです。アクセシビリティを担保するためにまず第一に重視すべきはURIであり、alt属性やその他のメタデータはその補助にすぎないというのが私の考えです(もちろん、そのURIを解釈できない処理系のためにalt属性などを付随させるのは有用であり、その努力は尊敬されるべきものです)。

実際に組み込んでみる

さて、前置きはほどほどにして、インストール手順に進みましょう。TPにLua拡張を組み込むには、ビルド時に以下のような指定を行います。もちろん、その前提としてLuaをインストールしてあることが必要です。

$ ./configure --enable-lua
$ make
$ sudo make install

そして、テンプレートファイル promenade.tmpl の冒頭にある設定に以下の行を加えます(最新バージョンのデフォルトテンプレートではその行が最初からあります)。

[% CONF scrext "hogehoge.lua" \%]

こうすると、TPの起動時に hogehoge.lua というファイルが読み込まれて、それに記述された関数が各種のトリガに応じて呼び出されるようになります。トリガと関数の仕様は以下のようになっています。該当の関数が実装されていない場合は単に無視されます。

  • _begin() : DBを読み込む前に呼ばれます。引数なし。戻り値はテンプレートのbeginmsgという変数に格納されます。
  • _end() : DBを閉じた後に呼ばれます。引数なし。戻り値はテンプレートのendmsgという変数に格納されます。
  • _procart(wiki) : 各記事の表示データを作る直前に、そのWiki記法データを表示用に加工するために用います。引数は元のwiki記法文字列で、戻り値は加工後のwiki記法文字列です。
  • _procpage(html) : ページ全体のHTMLを表示する直前に、HTMLを表示用に加工するために用います。引数は元のHTML文字列で、戻り値は加工後のHTML文字列です。

また、以下の機能が組み込まれています。

  • _params : CGIスクリプトの呼び出しパラメータがテーブルとして保存されているグローバル変数。
  • _user : ユーザの認証情報(nameとpassとinfo)がテーブルとして保存されているグローバル変数。
  • _strstr(src, pat, repl) : 文字列の単純な検索と置換を行う組み込み関数。srcは対象文字列で、patはマッチングパターンで、replは置換文字列。replが省略された場合は一致の検査のみを行う。
  • _regex(src, pat, repl) : 正規表現による検索と置換を行う組み込み関数。srcは対象文字列で、patはマッチングパターンで、replは置換文字列。replが省略された場合は一致の検査のみを行う。

Luaの標準ライブラリにも文字列検索や文字列置換の機能はあるので_strstrと_regexは不要とも言えますが、標準のものはびっくりするほど使いにくいので、_strstrと_regexは重宝することになると思います。ちなみにそれらはTCのLuaバインディングにもおまけで組み込まれています。

サンプル

ネタでいくつか実装してみましょう。ログインユーザに挨拶文を出すためのスクリプトは以下のようになります。

function _begin()
   local name = "World!!"
   if _user then
      name = _user.name
   end
   return "Hello, " .. name .. "!!"
end

各記事をラムちゃん風にするためのスクリプトは以下のようになります。

function _procart(wiki)
   wiki = _strstr(wiki, "ります。", "るっちゃ。")
   wiki = _strstr(wiki, "です。", "っちゃ。")
   wiki = _strstr(wiki, "だ。", "だっちゃ。")
   return wiki
end

不適切な語を含む記事のタイトルにラベルをつけるスクリプトは以下のようになります。

function _procart(wiki)
   wiki = _strstr(wiki, "Tokyo", "Osaka")
   if _regex(wiki, "*sex") or _regex(wiki, "*fuck") then
      wiki = _regex(wiki, "^#!", "#! [sexual] ")
   end
   return wiki
end

簡易的なアクセスカウンタは以下のようになります(排他制御をしていないので本気利用はできませんが…)。上記3例はJavaScriptでもできますが、この例はLuaならではと言えるでしょう。

tmpfile = "/tmp/promscrcount.txt"

function _begin()
   local file = io.open(tmpfile, "r+")
   local count = 0
   if file then
      count = file:read()
      file:seek("set", 0)
   else
      file = io.open(tmpfile, "w")
   end
   if not count then
      count = 0
   end
   count = count + 1
   file:write(string.format("%d\n\n", count))
   file:close()
   return "The access count is " .. count .. "."
end

まとめ

Tokyo PromenadeにLuaを組み込む方法とその思想的背景について説明しました。クライアント側で完結するカスタマイズはJavaScriptでやった方が簡単だしサーバの負荷も軽くなるのでよいのですが、それがサーバ側の能力が必要な場合に組み込みのLuaで補うというのは合理的な選択だと思います。そして、言及するリソースの種類に応じたプレゼンテーションを人間が書き分けるのではなく、記事にはURIのみを記述して表示時にスクリプトが適切なプレゼンテーションを生成するということの意義についても伝われば幸いです。

TPそれ自体をスクリプト言語で書けばいいじゃんとか、サーバ側のスクリプト言語もJavaScriptでいいじゃんとか、いろいろご意見があると思います。CとLuaの組み合わせが絶対最強だと主張するつもりはありません。でも、自分で使ってみた限りでは、そこそこイケてるんじゃないかと思っています。

新生児が家に来たおかげで生活が一変して激太りしたmikioです。さて、一部のWebマニアには好評をいただいているTokyo Promenadeですが、今回はその追加機能について語ります。

サイト移転

Tokyoシリーズの配布サイトを新規設置したホームページに移しました。そもそも、Tokyo Promenadeはそこで使うためのCMSとして作ったのです。以後、Tokyo CabinetTokyo TyrantTokyo DystopiaTokyo Promenade、そしてまだ見ぬTokyo製品のパッケージとドキュメントはそこで配布します。

TPの最新版を使ったブログもそこで書いていく所存です。使い勝手はそちらか、以前設置したデモサイトで確認していただけます。

Atomフィード

最近人気のあるブラウザはRSSAtomの購読機能をデフォルトでサポートしています。また、Google ReaderLivedoor Readerなどのフィード購読サービスを使って効率的にブラウジングを行う習慣も一般化してきています。そういった背景もあって、イマドキのCMSではRSSなりAtomなりのフィード機能を実装していないとユーザにそっぽ向かれてしまうようになってしまいました。個人的にはXHTMLに標準語彙のメタデータを埋め込めばRSSやAtomなど不要なはずだと思っているのですが、世の流れには逆らえません。

ということで、TPの最新版ではAtomフィード機能が実装されています。TPのAtomフィードには以下の5種類のビューがあり、それぞれ対応するHTMLページにあるオートディスカバリから取得することができます。

  • 作成日時順一覧: サイト全体の記事を作成された日時の降順で提示
  • 最終更新日時順一覧: サイト全体の記事を最後に更新された日時の降順で提示
  • 最終コメント日時順一覧: サイト全体の記事を最後にコメントされた日時の降順で提示
  • 記事毎の本文とコメント一覧: 個々の記事に着目して本文とコメントを各エントリとして提示
  • 検索結果: 検索ページの任意の結果の最新情報を提示

デフォルトでは各フィードは降順(新しい順)で提示されますが、ソート順のパラメータ(order)の値に「_」を接頭させると昇順(古い順)にできるという裏技もあります。例えば、「order=cdate」だと作成日時降順なので、「order=_cdate」にすると作成日時昇順になります。

なお、Atomの各エントリおける時間情報のメタデータは、更新日時(updated)と作成日時(published)しかありません。一方で、TPでは、記事の最終更新時刻と最終コメント時刻は別々の属性として管理しているので、そのままだと最終コメント日時順一覧をうまく表現できません。Atom Threading Extensions(RFC 4685)という語彙が標準化されているのでそれを使うことも試みましたが、それに対応したユーザエージェントがそれほど普及していないので現時点では断念しました。仕方ないので、最終コメント日時順の場合に限って、各記事の最終コメント日時を最終更新時刻(updated)として配信するようにしています。結果として、最終コメント日時順のフィードを常用すると2ちゃんねる的なフローティングが発生するフィードになります。

更新プラグイン

サイトが更新された際にはブログ検索エンジンやフィード購読サービスに対して更新通知(ping)を打ってクローラを呼び寄せるというのがブログ等のCMSでは一般的です。しかし、pingを打つURLやプロトコルは対象のサービスによってまちまちなので、具体的な指示を設定ファイルだけで行うことは現実的ではありません。そこで、TPでは、各記事が更新された際に、その記事の情報を渡して任意のコマンドが呼べるようにしています。そのコマンドとしてシェルスクリプトを指定して、その中でpingを打つなり差分をバックアップするなりの任意の処理を行うことができます。具体的な仕様は以下にになります。

  • 設定ファイルのupdatecmdという変数でコマンド名(パス)を指定する。
  • 記事が更新された際にそのコマンドが呼び出され、以下の7つの引数が渡される。
    1. 操作種別 : new/update/comment/removeのいずれか
    2. 記事ID : 対象記事のID番号
    3. 更新後データ : 更新後の記事のWikiデータを保存したキャッシュファイルのパス
    4. 更新前データ : 更新前の記事のWikiデータを保存したキャッシュファイルのパス
    5. タイムスタンプ : 更新時刻のマイクロ秒
    6. ユーザ名 : 更新操作を行ったユーザの名前
    7. スクリプトのURL : 実行されているCGIスクリプトの絶対URL
  • コマンドの終了コードが真(0)なら何もせず、偽(0以外)ならエラーメッセージを出力する。

更新プラグインのサンプルとして、すべての記事の更新履歴をパッチ(diffデータ)として保存してトレーサビリティを確保するための「promupdiff」というスクリプトが同梱されています。また、GoogleとYahooのクローラに更新通知を送るための「promupping」というスクリプトもあります。なお、promuppingの方は、今話題の「Pubsubhubub」の公開ハブにも更新通知を送っています。この仕組みが普及すれば、CMS側としてはいろんなサービスにpingを打つプラグインを量産しなくて済むようになって嬉しいし、購読サービス側としてはクローラの実装が簡単になってかつリアルタイムで情報が取得できて嬉しいことになりそうなので、うまいこと流行ってほしいところです。

object要素

従来から、「@」の後ろにURLを書く記法によって、記事の中に画像を埋め込むができました。しかし、埋め込みたいのは画像だけではなく、動画や音楽を埋め込みたいこともあるでしょう。ということで、「@!」の後ろにURLを書く記法を追加しました。そうすると、img要素の代わりにobject要素が使われ、src属性の代わりにdata属性が使われます。width属性やheight属性を「|」で区切って指定できることは同様です。その後ろにさらに「|」で区切ったフィールドを追加すると、param要素を子要素として持たせることができます。例えば、以下のような変換ルールになります。

@! http://example.com/foo.mov|400|300
 ↓
<object data="http://example.com/foo.mov" width="400" height="300"
  type="video/quicktime"></object>
@! http://example.com/foo.class|400|300|nick|mikio|sex|male
 ↓
<object data="http://example.com/foo.class" width="400" height="300">
<param name="nick" value="mikio">
<param name="sex" value="male">
</object>

この方法はかなり汎用性が高く、FlashやJavaアプレットを埋め込むこともできますし、MIDIを埋め込んでBGMを鳴らすこともできますし、Google Mapsも埋め込むことができます。

提示されたobject要素をどうレンダリングするかはユーザエージェントに任されることになっており、ブラウザ毎のサポート状況の違いが激しいことになっていますが、画像以外のデータを埋め込むためのstrict標準に準拠した方法はobject要素しかないのが現状です。しかし、近い未来を想定したアクセシビリティを考えると、あくまで「任意の形式のデータを埋め込む」という意味のWiki記法を定義して、それに応じてobject要素を提示するという方針が最善だと思います。applet要素、embed要素、iframe要素など、個々のデータ形式に応じて要素を使い分けないと表示できないブラウザもまだまだ多くあります(例えば上記のGoogle Map埋め込みはIEだと表示できない)。しかし、個々のブラウザへの最適化はonloadイベントを拾ってJavaScriptでDOMに干渉することによって可能なので、その方針で適宜サポート範囲を広げることで、「現在のアクセシビリティ」と「保守性」すなわち想定される近未来のアクセシビリティの両立を図る所存です。

画像配置の拡充

また、「@」記法も「@!」記法も、「@|」や「@!|」などとして「|」を後置させることによって画像などを段組みさせることができるようになりました。デモとして、段組みなどの凝った画像配置をしたページをご覧ください。「|」が1つだと2段組み、2つ並べると3段組み、3つ並べると4段組み、4つ並べると5段組みになります。例えば、TCのロゴを3段組みで表示したい場合には以下のようにします。

@|| http://1978th.net/tokyocabinet/logo.png
@|| http://1978th.net/tokyocabinet/logo.png
@|| http://1978th.net/tokyocabinet/logo.png

なお、段組みはスタイルシート(CSS)のみで実現しているので、テキストブラウザなどのCSS非対応ブラウザで見る場合には段組みでなく上下に並べられて表示されることになります。

検索機能の強化

今年の花火大会をどこに行くか決める時に、去年の花火大会について書いた記事を読み返したくなるようなユースケースを想定してみましょう。そのような場合は記事の作成日時を指定して検索できると便利です。従来から、記事名、著者名、タグ、本文のそれぞれの全文検索とそれらを統合した全文検索機能をサポートしているTPですが、最新バージョンからは日付を指定した検索ができるようになりました。例えば、検索対象を「creatin date」にして検索条件を「2008」にすると、2008年中に執筆した記事の一覧を得ることができます。なお、「2008-07」とすると2008年7月中という条件になり、「2008-07-29」とすると2008年7月29日中という条件になります。

また、検索ページのファーストビューで、全記事の名前の一覧「recent articles」と、記事を執筆日時毎にまとめた検索条件の一覧「archives」を表示するようにしました。前者は最近どんな記事を書いたか手軽に確認するのに便利ですし、後者は上述の日付検索を手軽に行うのに便利です。

サイドバー

ページの右端にサイトの更新情報やナビゲーションを表示する「サイドバー」を実装しました。これを実装するかどうかは正直かなり悩みました。というのも、シンプルさを何よりも大切にするTPのコンセプトにおいて、必ずしも日常的に使うわけではない機能群を常にファーストビュー(最初に目に入る画面)に表示するというサイドバーの存在は違和感があるからです。サイドバーには更新日時や新着記事の一覧や新着コメントの一覧などが表示されるわけですが、同じ情報はタイムラインや検索ページにあるので、サイドバーは冗長な存在とも言えます。そして、そこに張られているリンクを実際にクリックすることはほとんどありません。このブログの読者の皆さんにおいても、右端にあるサイドバーのリンクをクリックしたことのある人の割合はかなり低いのではないでしょうか。

このように、存在に必然性がなく冗長で画面(視線移動)の無駄とも言えるサイドバーですが、にもかかわらずなぜサポートしたかと言うと、TP以外のCMSに慣れたユーザ(=読者)の暗黙的期待に答えるためです。また、検索系やナビゲーション系の機能の価値は利用頻度だけで量るべきではなく、執筆した情報にいつでも到達できるという「安心」を提供できるという利点も考慮しました。ただし、ある程度規模が大きくて更新が頻繁なサイトでないとサイドバーは心理的意味すら持たないので、デフォルトでは無効になっています。設定ファイルで有効にした場合にのみ、サイドバーが表示されます。

なお、このブログ(WordPress)に見られるように、検索のところで述べた「archives」に相当するコラムをサイドバーに表示する実装も多くありますが、TPではそれは回避しました。「recent articles」に乗らないくらい古い記事を日付指定してまで読むというのは、頻度もとても低いですし、上述の花火大会の例のようにかなり能動的な欲求から起こる行動なので、「search」リンクの1クリックを要求されても苦にはならないでしょう。また、「archives」のコラムは過去の情報の増大とともに占有領域を増すことになって見栄えがよくない(安定しない)という理由もあります。さらに、全記事のレコードを走査しないと日付毎の集計ができないので、その演算を全ページでやるとなると応答時間に影響する可能性が否定できないという理由もあります(キャッシュすれば問題ないのですが、それもあんまりやりたくない)。

その他の小技

ここまで述べた以外にも細かい改善をしています。iPhoneで見た場合に画面を効率的に使うようにしているとか、印刷した場合だけ不要なナビゲーションを非表示にするとか、ブラウザの言語設定を見てヘルプ記事を出しわけたりとか、HTMLのmeta要素をリッチにしたりとかです。ブラウザのキャッシュを効率的に利用できるようにHTTPのCache-Control設定も工夫しました。

まとめ

Tokyo Promenadeの付加機能について説明しました。どれも自分でしばらく運用してみて欲しくなった機能ですので、類似のユースケースの人には嬉しい機能だと思います。まだまだ改良の余地はありそうですが、凝り出すと止まらないのがUIの世界です。いくら高機能なCMSを使ったところで内容がショボいのでは意味がないので、ここらで機能追加は自重して、本来の研究開発と情報発信をする仕事に戻りたいと思います。

先日、待望の長女が誕生したmikioです。あまりにかわいいから育児ブログでもつけようという魂胆ではありませんが、今回は自作のCMSであるTokyo Promenadeについて語ります。

tp-help-screen

Tokyo Promenadeとは

以前の記事で、Tokyo Cabinet(TC)を使ったCMSを作ることを予告しましたが、Tokyo Promenade(TP)がまさにそれです。TCのテーブルデータベースを使って記事を管理する軽量なコンテンツ管理システム(CMS)の実装です。例によってC言語のみで記述され、libc以外の全実装が "made by mikio" な製品です。

読み方は「東京プロムナード」です。プロムナードとは散歩道のことですが、東京メトロの広告に出てくる宮崎あおい的なキャラが写真付きブログを書いちゃうようなユースケースをイメージして名づけました。まあ実装はそんな洒落た感じとはほど遠いですし、実際に私が使う際には小賢しい技術ネタを書きなぐることになるでしょうけども…。

まずはデモサイトにアクセスしてみてください。ヘルプに基本的な使い方は書いてあります。デモサイトなので、SandBoxの記事を追加したり編集したり削除したりしていただいて構いません。

掲示板なの? Wikiなの?

この質問はFAQでありつつ、回答に困ってしまうものの代表です。TCのようなスキーマレスのデータベースを使っている場合、掲示板(またはブログ)とWikiのデータ構造は全く同じであるとみなすことができます。双方とも、各記事は内部IDで識別され、名前(タイトル)やタイムスタンプや著者名などの属性を持つレコードとして記事を記録し、その属性によって表示対象と並び順を定義したビューを構成できて、ユーザからの入力をもとにビューの決定と記事の操作を行うシステムです(まあそのレベルで抽象化したらほぼ全てのDBアプリはそういうアーキテクチャに収まるとも言えますが)。

掲示板とWikiの主な違いは、トップページで提示するビューが「時系列リスト」なのか、人間の編集者がおすすめする「フロントページ」なのかということです。TPのデフォルト設定では掲示板として時系列リストを提示する掲示板仕様になっていますが、設定ファイルで任意の記事をフロントページを指定するとWiki仕様になります。なお、作成日順の時系列リストを提示すると普通の掲示板として使えますが、更新日順の時系列リストを提示すると「2ちゃんねる」のようなフローティングスレッド掲示板風に使うこともできます。

ということで、質問への回答としては「掲示板とWikiはビューの違いに過ぎないので、あなたがどう使うかでどちらであるかが決まるのです」ということになります。個人的には名前とか分類とかはどうでもよくって、「HTMLとかHTTPとか難しいことなんて知らなくても誰でも簡単にお洒落なWebサイトが作れる」というCMSの醍醐味が味わえればそれでいいかなと。

TPの素敵ポイント1:シンプルでロジカルなUI

私はw3mというテキストブラウザが結構好きで、実はTPのユーザインターフェイスはw3mで最も見やすいように調整されています。しかし市場のブラウザシェアを考えればIEやFirefoxやSafariでそこそこ綺麗に表示できないと話になりませんので、CSSを駆使してそれらでもできるだけ読みやすくなるように努力しています。華美な装飾を施すのではなく、各記事の著者がWiki記法で記述した論理性をいかに直感的に読者に伝えるかということを追求しています。

テキストブラウザで読みやすくするためには論理構造のみをHTMLに記述するという選択が自然になされるため、結果的にロジック(HTML)とスタイル(CSS)の分離が徹底されることになります。また、ロジックの部分をできるだけ論理的に記述してもらうために、Wiki記法では論理的に必要だと思われる表現のみを厳選しています。そういった仕様策定のヒントにしたのは、研究者が論文などを書くときによく用いられるLaTeXというシステムです。LaTeXの文書でよく使う見出しやリストや図表といった構造のみがWiki記法で選択できるようにしています。CSSによるスタイルも、LaTeXっぽい見栄えになるように調整しています。

TPの素敵ポイント2:アクセシビリティ

ロジックとスタイルを分離した結果として、いわゆるフルブラウザはもちろん、テキストブラウザでも、視覚障害者用の音声ブラウザでも、モバイル端末でも、プリンタでも、それぞれの環境なりに最善を尽くした表現ができるようになっていると思います。このように様々な環境および様々な境遇の人に対して情報の授受ができる程度を指してアクセシビリティと言うことがありますが、世に数多あるCMSの中でTPはかなりマシな方だという自負があります。

ただ、デフォルトの状態では多様な環境で読みやすいようにしている分、個々の環境に最適化できているわけではありません。最適化の要求があるユースケースでは、出力用テンプレートをカスタマイズすることで対処できるし、またそうすべきだと考えています。音声ブラウザを使っていない私が音声ブラウザに最適な表現を定義するのは無理ですし、iPhoneを持っていない私がiPhoneに最適化された表現を定義するのも辛いものがありますので、個々のユースケースへの最適化はユーザの皆さんに任せるのが現実的です。出力用テンプレートはTCのテンプレート直列化機能をそのまま使って実装されています。テンプレートファイルはHTMLの中に [% ... %] という形式のディレクティブを記述した単純な構造なので、プログラマでなくても簡単にHTMLレベルの構造を変更することができます。

TP以外の多くのCMSでは記事にHTMLを直接記述できる機能があります。そうすると備え付けのWiki記法では不可能な表現方法を自由に使うことができるからです。しかし、それをやってしまうと論理構造がめちゃくちゃになってしまうし、validなHTMLを書けるユーザの割合は著しく低いので、TPでは禁止しています。そのおかげで、TPが出力するHTMLはXHTML 1.0に完全準拠することが保証されます。したがって、JavaScriptやXSLTで加工するのも容易ですし、スクレイピングをして外部アプリケーションを作るのも容易です。

基本事項ではありますが、コンテンツを表示する際には以下の点に心がけるようにしています。

  • ブックマークの名前として識別しやすいtitle要素をつける。
    • 複数の記事をリスト表示する際にはサイト名と機能名をつなげた文字列
    • 記事単体を表示する際にはサイト名と記事名をつなげた文字列
  • 各ページで最も強い見出しをh1要素にする。そしてh1要素は必ず0個か1個にして複数は置かない。
    • 複数の記事をリスト表示する際にはサイト名をh1にし、各記事の名前はh2、見出し1以降はh3以降
    • 単体の記事を表示する際には記事の名前をh1にし、見出し1以降はh2以降
  • 見出しには各記事のIDおよび見出しの階層を反映させたid属性を付与してリンクされやすくする。
  • データベース内の日付はカレンダー時間で保持するが、表示時にはローカル時間に変換する。

各記事のタイトルや見出しは特定のHTML要素に単純に変換させるわけではなく、ビューに基づく相対的な順位で要素が決められるというあたりが私なりのこだわりです。とはいえCSSでスタイルを割り当てる際の利便性を考えて、記事毎の絶対的な見出し順位もきちんとclass属性として指示しています。

TPの素敵ポイント3:快適な画像挿入

テキストブラウザな人は画像とかあんまり興味ないでしょうが、世の一般的なブログなどではほぼ全ての記事に写真やイラストを載せるのがトレンドだというのも否めません。私が育児ブログをつけるとしてもおそらくそうすることでしょう。そのようなユースケースでは、画像をいかに美しく挿入できるかがCMSとしての良し悪しを左右することになります。

画像はさすがにWiki記法では記述できない(Base64で貼り付けるなどの方法は不可能ではないが現実的ではない)ので、ファイルアップロード機能を実装しました。また、アップロードした画像に任意の名前(デフォルトはローカルでのファイル名)がつけられますが、タイムスタンプで識別されるので同じ名前のファイルが複数個あっても問題ありません。

画像を記事に挿入する際には、「@」の後ろにURLを書くだけです。「http://」で始まるURLでWeb上にある任意の画像データを参照できるのはもちろん、「upfile:」で始まる内部識別子でTP内にアップロードした画像を参照することもできます。URLでなく内部識別子を使った方がサイトを移設した際のリンク切れを回避できるので有利です。

画像を挿入するような記事の著者はおそらくタブブラウザを使っているでしょう。記事を執筆すべく編集画面を開きながら、画像が挿入したくなった段階で別タブでアップロードファイルの管理ページを開き、検索機能やサムネイルを駆使して対象の画像を特定し、そこに張られた内部識別子を右クリックでコピーしてクリップボードに入れます。それから編集画面に戻って任意の位置にペーストすれば作業完了です。原始的なようですが、JavaScriptでURLを挿入するタイプの挙動だと元の編集画面が勝手にスクロールしてしまってイラつくことが多いので、敢えてこの素朴な方法を推奨しています。

挿入した画像の表示位置も簡単に制御できます。「@ hoge」と書けばその位置に画像がそのまま表示されますが、「@< hoge」と書けば左寄せ、「@> hoge」と書けば右寄せで表示されます。左寄せなら本文は右に回りこみ、右寄せならば左に回り込むようになります。結果として、雑誌の記事のような見栄えで写真を紹介できるようになります。

TPの素敵ポイント4:Wikipedia大好き

著者には既知だが読者に未知であるかもしれない一般的な概念を記事の中でわざわざ説明するのはだるいものです。一般的な概念であればだいたいのことはWikipediaで説明されているので、該当の記事にリンクを張れば十分です。ということで、リンク先に「wpja:」で始まる識別子を書くと、自動的にWikipedia日本語版のその名前の記事へのURLに直す機能があります。「wpen:」はWikipedia英語版へのURLになります。いわゆるInterWikiですね。小ネタのような機能ですがかなり時間の節約になります。

実装の苦労

TCのテーブルDBとテンプレート直列化機能とオブジェクトシステムがめちゃくちゃ強力なので、Wiki記法だのユーザ管理機能だのファイルアップロード機能だのを満載している割には、TPの全ソースコードは3000行未満に収まっていて非常にコンパクトです。なので、実装について語ることはあまりありません。ご興味のある方はパッケージ内の promenade.c を読めばだいたいの流れが理解できると思います。

敢えて苦労した点を挙げるなら、拡張子とMIMEタイプの対応表を手でハードコーディングしたりとか、時刻表現は10進数とW3CDTFとRFC1123形式を全てサポートしたりとか、クッキーを暗号化するためにRC4を実装したりとか、ファイルアップロードのmultipart/form-data形式のパーザを自分で書いたりとか、1バイトたりともメモリリークしないようにvalgrindでほぼ全てのコードを調べたりとか、全画面がXHTML 1.0でvalidかどうか確かめるためにxmllintとhtmllintで出力を調べたりとかがありました。それより何より、一番面倒だったのはWiki記法を解析してHTMLに直すコンバータを書くことでした。得にリストがネストする構造が厄介で、途中でやめようかと何度も思ったものです。

とはいえ、全ての課題はクリアされ、現時点で私の欲しい機能が全て実装されているCMSが完成しました。スクリプト言語の処理系やデータベースサーバをインストールせずに使えるし、貧弱なマシンでも軽快に動くし、それでいてテンプレートをいじって簡単にカスタマイズできるし、シンプルで飽きの来ないデフォルトのデザインも付いてくるし、我ながら結構イイ感じです。

インストールしてみましょう

ここまで読んでみてTPを使いたくなってくれた人も少数ながらいるかと思いますので、インストール方法について超要約で説明します。WebサーバとTCが予めインストールされていることを前提とします。

# TPをダウンロードする
wget http://tokyocabinet.sf.net/promenadepkg/tokyopromenade-0.9.1.tar.gz

# TPをインストールする
tar zxvf tokyopromenade-0.9.1.tar.gz
cd tokyopromenade-0.9.1
./configure
make
sudo make install

# CGIスクリプトなどを置くベースディレクトリを作る
mkdir /home/mikio/public_html/cms
cd /home/mikio/public_html/cms

# TPの各種ファイルをベースディレクトリにコピーする。
cp /usr/local/libexec/promenade.cgi .
cp /usr/local/share/tokyopromenade/promenade.* .
cp /usr/local/share/tokyopromenade/passwd.txt .

# データベースファイルとアップロードファイル用ディレクトリを作る
prommgr create promenade.tct
mkdir upload

# CGIスクリプトがデータベースやディレクトリを更新できるように適宜設定する
chmod 666 promenade.tct*
chmod 777 upload

# 気が向いたら、ヘルプファイルをインポートする
prommgr import promenade.tct /usr/local/share/tokyopromenade/misc/help-ja.tpw

あとは、設置したCGIスクリプト「promenade.cgi」にWebブラウザでアクセスすれば使い始めることができます。デフォルトで管理者ユーザのアカウントが名前「admin」およびパスワード「nimda」として定義されていますので、それでログインして記事を書いたりユーザを作ったりファイルをアップロードしたりしてみてください。

promenate.tmpl」というファイルがテンプレートファイルになります。これをいじることで、出力されるHTMLのほぼ全てをカスタマイズすることができます。デフォルトでは掲示板スタイルの時系列表示がトップページに設定されていますが、テンプレートの冒頭にある「[% CONF ... %]」のディレクティブをいじってフロントページを設定するとWikiとして使うことができます。

まとめ

Tokyo Promenadeの概要について述べました。設定によって掲示板(ブログ)風にもWiki風にも使えるシンプルなCMSです。C言語だけでも、DBMだけでも、GNOMEなんたらやApacheなんたらを使わなくても、そこそこ実用的なシステムを作れることが伝われば幸いです。

シンプルっていいですよね。複雑なシステムってだいたいすぐに嫌気が差してしまいますし、特定のユーザのユースケースに特化した機能を操作の選択肢として全員に提示するのは、全体のユーザビリティを下げることにつながります。大多数のユーザが必要最低限の機能を迷わず使えるという大前提を確保した上で、慣れたユーザはその学習曲線に応じて個々のユースケースに最適化された使い方ができるようにするのが理想です。その理想に照らすとTPはちょっとシンプルさが行き過ぎた感もありますが、TPが想定するユーザ層である「ワープロでなくテキストエディタを使う人達」にとってはこれくらいがバランスポイントだと思っています。

TPの今後ですが、RSS配信機能やメールによる更新機能をつけたりといった今のトレンドに合った機能追加を行う予定です。あと、そもそもの開発の動機として英語ブログを立ち上げてTokyoシリーズについて語りまくるというのがあるので、近日中にやりたいと思っています。レンタルサーバとドメイン取得で最もコスト(手間含め)が低いオススメの方法があれば教えてください >識者。