先日、milter managerを用いたメールフィルタリングについて話してきました。milter managerというより、milter managerがベースにしているmilterという技術についての説明の方に重点をおいています。これは、日本でのmilter情報が不足しているのを少しでも解消したいということからです。
milterはPostfixも積極的にサポートを強化している有用な技術の1つです。メールに関わっている方はその仕組みや動作について把握しておいて損はないはずです。
それでは、コメント付きでとばしとばし資料の内容を紹介します。完全版はリンク先を見てください。資料のPDF・ソースもリンク先にあります。
milterについて説明する前に、まず、より一般的なメールフィルタについてまとめておきます。その後、メールフィルタの仕組みの1つであるmilterについて説明し、最後にmilter managerも利用したmilterの使い方について説明します。
外部から届いたメールをユーザに配信するのがメールシステムです。メールシステム内部では単純に届いたメールをそのままユーザに配信するのではなく、なんらかの処理をしてから配信します。そのため、ユーザには加工されたメールが配信されます。例えば、Receivedというメールヘッダを追加したりします。
つまり、メールシステムの中にはメールフィルタの機能が内臓されています。それでは、メールフィルタについてもう少し詳しくみていきます。
メールフィルタの実現方法にはいくつかありますが、大きく以下の3種類に分けられます。
それぞれの方法について図で説明します。
MTAに組み込む方法では、MTA内にメールフィルタの機能を内蔵させます。MTA単体で実現できます。
例えば、Postfixではaccess(5)やcidr_table(5)を用いてフィルタを適用することができます。
MTAのプラグインとしてフィルタを実現する方法では、MTAと別のプロセスにメールを通すことによりフィルタリングします。この別のプロセスがフィルタプラグインです。milterもこのタイプのメールフィルタの仕組みです。
MTAとプラグインのやりとり方法はMTA独自のことがほとんどです。SendmailやPostfixなど複数のMTAで動くmilterの方が珍しいといえます。milterが複数のMTAでサポートされているのは、既に多くの有用なmilterが存在しているからと思われます。
複数のMTAでメールシステムを構築することもできます。フィルタMTAを挿入する方法では、外部からのメールをユーザに配信するまでの間に、フィルタリングをするMTAにメールをリレーすることで、メールフィルタリングを実現します。MTA間のやりとりにはSMTPを使うので、どのMTAでも実現できる方法です。
アプライアンス製品などはこの方法でメールフィルタ機能を提供することが多いです。
それでは、それぞれのメールフィルタの実現方法による違いです。それぞれ特徴があるので、必要とされている機能や導入した後のメンテナンスなども考慮して、メールシステム毎に適した方法を選択することが重要です。
MTAに組み込む方法の利点はお手軽であるということです。組み込まれている機能はMTA毎に違いますが、組み込まれた機能で十分な場合はこの方法を利用するとよいでしょう。機能を追加する場合は、パッチを当てたり、バージョンアップをしたりすることになります。機能を追加する場合はメンテナンスのことをよく考えてください。
MTAのプラグインとしてメールフィルタを利用する方法の利点は、柔軟性です。多くの場合、プラグインではMTA本体が実現していることのほとんどすべてを実現できます。そのため、必要な機能をMTA本体を変更せずに追加することができます。MTA組み込み方法よりは導入が面倒ですが、システムや世間の変化にあわせたメールシステムを構築する場合は、機能追加や複数の機能を組み合わせられるこの方法を利用するとよいでしょう。
フィルタMTAを挿入する方法の利点は、どのMTAでも利用できることです。フィルタMTAを挿入する場合は、MTAが数珠つなぎになります。そのため、MTAを挿入・削除するたびに複数のMTAのリレー設定を変更する必要があり、面倒です。もし、必要な機能をすべて満たしたフィルタMTAを1つ導入するのであれば、この方法がよいでしょう。複数のフィルタMTAを導入する場合は導入・メンテナンスのことをよく考えてください。
以上のことをふまえると、以下のポイントをメールフィルタの実現方法を選択するために使えます。
メールフィルタは迷惑メール対策に利用されることが多いです。その観点から考えるとどの方法がよいでしょうか。
迷惑メールの配信方法は多様化しているため、1つの対策で完璧に防ぐことはできません。複数の手法を組み合わせて対応する必要があります。
また、これからも新しい手法で迷惑メールは送られ続けるでしょう。そうなった場合でも新しい対策を適用できることが求められます。
よって、迷惑メール対策には、複数の機能を組み合わせることが容易で、新しく機能を追加できるプラグインタイプの方法が適しているといえます。プラグインタイプの中でも、複数のMTAで利用でき、すでに多くの実装が存在するmilterがオススメです。ということで、milterの説明に入ります。
「milter」といっても文脈によっては微妙に異なるものを指すことがあります。「広義の○○」「狭義の○○」といわれたりするものと同じようなものです。
「milter」はメールフィルタの仕組み全体のことを指す場合と、フィルタプログラムのことを指す場合が多いです。「milterに対応したMTA」という場合は「メールフィルタの仕組み」を指していますし、「このmilterを使う」という場合は「フィルタプログラム」のことを指しています。
milterの説明に入る前に、混乱しないように別々の名前をつけておきます。ただし、正式な名前ではないことに注意してください。あくまで、わかりやすさのためにつけた名前です。
それぞれの関係を図示するとこうなります。
MTAとmilter間でやりとりするmilterプロトコルはSMTPと密接に関わっています。milterプロトコルでのコマンドのほとんどはSMTPでのコマンドに由来しています。
例えば、MTAがSMTPで「EHLO」コマンドを受け取ったらmilterにも対応する「helo」コマンドが送られます。その時、EHLOで受け取ったFQDNも一緒に送られます。
また、milterプロトコルはSMTPと平行して動作するのも重要なポイントです。MTAがメッセージすべてを受信してからmilterにコマンドを送るのではないので、「helo」コマンドのときにmilter側でrejectすれば、MTAも「EHLO」コマンドのときにrejectし、「MAIL FROM」や「RCPT TO」などそれ以降のコマンドを受けとりません。
milterシステムでは複数のmilterを同時に利用することができます。複数のmilterを利用する場合もSMTPと平行して動作する点は変わりません。
複数のmilterを利用する場合に注意することは、それぞれのmilterはコマンド毎に順番に動くということです。1つめのmilterの処理が終わってからはじめて2つめのmilterにコマンドが送られます。
1つめのmilterの処理結果が2つめのmilterに影響を与えます。例えば、1つめのmilterで本文を変更したら、2つめのmilterには元の本文ではなく1つめのmilterが変更した本文が渡されます。DKIMなどメッセージが変更されると問題があるような処理をする場合はmilterの順序に気をつけてください。
例えば、DKIMの場合は、送信するメールの署名も受信したメールの署名の検証も一番外部に近い部分で行います。署名を行うmilterは、他のすべてのmilterが処理を終わった後に適用しなければ署名が壊れてしまうため、一番最後のmilterにします。署名を検証するmilterは、他のmilterがメッセージを変更する前に適用しなければ検証に失敗してしまうため、一番最初のmilterにします。
以上がmilterの動作です。それでは、milterの使い方について進みます。
milterシステムを使い込んでいくといくつか気になるところがでてきます。milterシステムはプラグインタイプなので他の方法に比べて機能の追加は容易ですが、さすがにmilterの数が増えてくると面倒になります。
milterはすべてのセッションに対して適用されます。ホワイトリストの管理は各milterの役割になります。しかし、複数のmilterを利用している場合は、複数の形式でホワイトリストを管理することになりメンテナンスが面倒になります。
これらの問題点を解決するソフトウェアがmilter managerです。
milter managerはMTA側に足りない機能を補って、milterをより使いやすくするソフトウェアです。MTA・milterどちらにも変更を加えずに導入することができるため、既存の環境を壊すことなく導入できます。
クリアコードの他のソフトウェアと同様、milter managerもオープンソースソフトウェアとして公開しているので自由に利用することができます。
milter managerはMTAとmilterの間で動作します。MTAからのmilterプロトコルのコマンドをmilterに転送し、milterからのレスポンスをMTAに転送します。MTAとmilterの間でやりとりを制御することができます。
milter managerはmilterとして実装されているため、MTAには1つのmilterとしてみえます。また、milter managerはMTA側のmilterプロトコルも実装していて、その機能を使ってmilterへコマンドを転送しています。よって、milterからはmilter managerがMTAのようにみえます。このため、MTAもmilterも変更なしでそのまま動作することができるのです。
milterシステムの設定・管理にいくつか作業が必要になります。
milterを追加する場合は、milter本体を設定した後にMTAにmilterを登録する必要があります。複数のmilterを同時に利用している場合はmilter本体の設定のみをして、MTA側に登録することを忘れてしまうこともあります。
milter managerはそのような部分を自動化し、単純なミスを防ぎます。/etc/以下を走査し、インストールされているmilterを検出したら自動で登録します。また、インストールはされているが無効にされている場合もそれを検出し、自動でそのmilterを使用しないようにします。
milterシステムは複数のMTAでサポートされていますが、完全に同じサポート状況ではなく、MTA毎に異なる部分があります。そのため、milterによってはSendmailでは動くが、Postfixでは動かないというのもありました。(最近はそのようなmilterは減ってきています。)
milter managerはMTAとmilterの間で動作します。その位置でSendmailとPostfixのmilterシステムのサポートの違いを吸収するため、同じmilterを変更せずにそのまま動作させることができます。
他にも、Sendmailはmilter毎にタイムアウトなどのパラメータを設定できるが、Postfixはmilter全体の設定なってしまうなどといった違いがあります。このような違いも、milter managerがmilter毎にパラメータを管理することにより、PostfixでもSendmailと同じようなmilterシステムサポートを利用することができます。
このようにmilter managerを導入することにより、milterシステムの管理が負担を減らすことができます。milter managerはMTAとmilterの間に入って動作するため、MTA・milterのどちらも変更せずにmilterシステムのサポートをより充実させることができます。このため、メールシステム全体の管理の負担も減ります。
前述の通り、milterはすべてのメールに対して適用されるため、milter全体でホワイトリストを共有したり、ユーザ毎に適用するmilterを選択することが面倒になります。それぞれのmilterにまたがって設定をする必要があります。
milter managerを使うとこの問題を解決することができます。MTAはすべてのmilterを適用しますが、milter managerはSMTPセッションの情報を使って動的に適用するmilterを選択する機能を提供しています。また、LDAPやRDBなどからもデータを取得することができるため、メールシステム外にあるアカウントシステムと連携した制御をすることもできます。
この機能を使うことにより、milter全体で共有するホワイトリストやユーザ毎にどのmilterを適用するかということをmilter managerレベルで一括管理できます。これにより、ユーザからの要望や利用状況に合わせた柔軟なメールシステムをメンテナンスしやすい構成で運用できます。
前述の通り、milter managerはmilterシステムをより使いやすくする機能を提供します。しかし、それだけにとどまらず、さらにメールシステムをより使いやすくする機能も提供します。その1つがtarpit問題の解決方法の提供です。
tarpitとはSMTPクライアントへのレスポンスを(RFCの範囲内で)わざと遅らせることにより、すぐに切断してくる迷惑メール送信SMTPクライアントからのメールを受信しないようにする迷惑メール対策手法です。
迷惑メール送信側はタイムアウト時間をRFCで示されている時間よりも短めに設定していることが多いというデータがあります。これは、できるだけ短時間で多くの迷惑メールを送信したいというのが理由ではないかと考えられています。この特徴にあわせた迷惑メール対策手法がtarpitです。
tarpitを使うことにより多くの迷惑メールを受信せずにすみますが、同時SMTPセッション数が増加し、リソース消費量が増えるという問題があります。1〜2分程度のレスポンス遅延を導入することにより、2倍程度セッション数が増えるというデータもあります。
この問題は、SMTPクライアントが切断したらすぐにSMTPセッションを終了することにより緩和することができます。Postfixなどでtarpitを実現すると、SMTPクライアントの切断を検出せずに、指定した時間までSMTPセッションを維持してしまいます。そのため、必要以上に同時SMTPセッション数が増加してしまうのです。
milter managerにはSMTPクライアントが切断したらすぐにSMTPセッションを終了する機能がついています。
milter managerはmitlerプロトコルでMTAからSMTPクライアントのソケット情報を受け取っています。それとnetstatコマンドの結果*1を照合することによってMTA外からSMTPクライアントの切断を検出します。切断を検出したらMTAとmilter双方に処理の中止を通知することですぐにSMTPセッションを終了することができます。
milterをベースとした柔軟なメールフィルタリングを実現するための仕組みを説明しました。
メールフィルタリングには以下のような仕組みがあり、それぞれ特徴があります。
新しい迷惑メール対策にも柔軟に対応していくメールシステムにはプラグインタイプがオススメです。プラグインタイプの仕組みの1つとしてmilterを説明しました。
milterはSMTPと密接に連携している仕組みです。milterプロトコルについても説明し、milterがどのような流れでフィルタリングを行うかも示しました。すでに多くの迷惑メール対策手法がmilterとして実現されているため、今すぐmilterを使って迷惑メール対策を組み込んだメールシステムを構築することができます。
最後に、milterシステムをより使いやすくするソフトウェアとしてmilter managerを紹介しました。milter managerを導入すると、設定の自動化や一元管理によりメンテナンスが容易なメールシステムが構築できることを説明しました。
組織に合わせたメールシステムを構築する場合があれば参考にしてみてください。
*1 Linux上では/proc/以下の情報を利用することも考えられます。
今年も肉の日がきましたね。
Ruby/groongaの0.9.0がリリースされました。リリース後すぐにgroongaの新しいバージョン0.1.6がリリースされたため、0.1.6に対応した0.9.1がすぐにリリースされました。gemで自動インストールされるgroongaのバージョンが0.1.6になっているだけで、Ruby/groongaの機能は変わっていません。
いつも通り、以下のコマンドでインストールできます。システムにgroongaがインストールされていない場合は自動でダウンロードしてインストールします。
% sudo gem install groonga
全文検索エンジンgroongaの特徴はgroongaのドキュメントを参照してください。
前のリリースでは0.0.7だったバージョン番号が一気に0.9.1まであがっています。バージョン番号から想像できる通り、初のメジャーリリース1.0.0を意識しはじめたということです。
0.9.x系列では1.0.0に向けて以下の2点を重点的に開発していきます。
APIを改良するため、以前のバージョンとは互換性が壊れることがありますが、今後より便利にRuby/groongaを使うために、今のうちに積極的に改良していく予定です。使ってみて、「ここがこうなっていたらもっと使いやすい」、「こういうAPIがあると便利」などという意見があったらぜひお知らせください。
Groonga::Context.default#[]
のショートカットとしてGroonga.[]
を導入するなど、もうすでに便利なAPIの追加は始まっています。
現在のRuby/groongaにはまだ高速化の余地があります。あまり意識せずにスクリプトを書いてもなるべく高速に動作するようにライブラリ側でできることはなるべくライブラリ側で頑張る方向で開発していく予定です。ユーザが使いやすくなるように処理系が頑張るというのはRuby本体と同じ方向です。
今年も年に一度の肉の日がきました。
Ruby/groonga 0.9.1がリリースされています。Rubyで全文検索システムを構築したい場合はRuby/groongaも検討してみてはいかがでしょうか。今のうちに改善案をだしておけばメジャーリリース時にはそれが反映されてより便利に高速な全文検索システムを構築できるようになるかもしれません。
先日、LOCAL DEVELOPER DAY '10 WinterでRubyでmilterを作る方法について話してきました。どのタイミングでどのmilterプロトコルのコマンドが発行されるかについても説明しているので、Rubyではなく(libmilterを使って)Cでmilterを実装する場合にも参考になる部分があるはずです。むしろ、Rubyとmilterの組み合わせについて話している部分は薄めです。これは、Rubyそのものとmilterの仕組みを理解していればRubyとmilterを組み合わせることは容易だからです。
少しgroongaについてもふれています。
それでは、ダイジェストで資料の内容を紹介します。完全版はリンク先を見てください。資料のPDF・ソースもリンク先にあります。
具体的にRubyでmilterを作る話に入る前に、まず、前提となる知識を確認します。
はじめにメールフィルタ、次にメールフィルタの仕組みの1つであるmilterについて簡単に説明します。その後、一度milterから離れてSMTPについて説明します。これはmilterの動作を理解するためにはSMTPの動作も知っておく必要があるからです。SMTPの動作を確認したらそれをふまえてmilterの具体的な動作を説明します。
ここまできたらRubyでmilterを作るための下準備は整っているはずです。実際に1つRubyでmilterを作ってみます。
説明の途中にいくつか確認ポイントがあります。それぞれの技術は他の技術をベースになりたっているので、ベースとなっている技術をおさえていくことが、理解してしっくりくるためのコツです。
それぞれの確認ポイントをゴールとして最終的な「Rubyでmilterを作れる」ようになるゴールまでたどりついてください。
メールシステムとは外部とユーザ間でメールを配信するシステムです。すべてのメールシステムではそのままメールをやりとりするのではなく、メールを配信するまでのあいだに、メールに対してなんらかの処理を実行します。つまり、すべてのメールシステムにはメールフィルタ機能が備わっています。
メールシステムでメールフィルタを実現する方法はいくつかありますが、その1つがMTA(メールサーバ)のプラグインとして実現する方法です。この方法のメリットはMTAを変更せずにメールフィルタ機能を変更できることです。milterはこのタイプで動作するメールフィルタです。
メールフィルタはメールシステムが持っている必須機能の1つです。その実現方法としてMTAのプラグインとして実現する方法があり、milterもその方法で実現されているメールフィルタです。
それでは、milterの概要について説明します。
milterの名前の由来は「mail filter」です。milterは汎用的なメールフィルタの仕組みのため、同じメールフィルタを異なるMTAと一緒に使うことができます。
Sendmailを用いているメールシステムではmilterを利用していることが多く、milterをサポートした商用のメールフィルタも多く存在します。最近ではPostfixのmilterサポートがリリース毎に改善されていっているため、Postfixを用いたメールシステムでもmilterを利用するケースが徐々に増えています。
「milter」は文脈によって異なるものを指すことがあります。そこで、ここでは混乱を避けるために異なる名前で呼ぶことにします。
まず、メールフィルタそのものを「milter」と呼びます。
メールフィルタとMTAは別プロセスで動作するため、プロセス間通信でフィルタ対象のメールやフィルタ結果などをやりとりする必要があります。そのやりとりのきまりを「milterプロトコル」と呼びます。
そして、「milter」と「milterプロトコル」をサポートしたMTAを含んだメールフィルタの仕組み全体を「milterシステム」と呼びます。
「milter」といった場合は「メールフィルタそのもの(ここでいうmilter)」という意味で使う場合と、「メールフィルタの仕組み(ここでいうmilterシステム)」という意味で使われる場合が多いです。「milter」という単語が使われている場合はどちらの意味かを判断できるようになってください。
milterプロトコルはSMTPと密接に関連したプロトコルです。そのため、milterプロトコルについて説明する前に、SMTPについて確認します。
SMTPは以下の4つのコマンドが基本となるシンプルなプロトコルです。
まず、「HELO」で接続したSMTPクライアントの情報を伝えます。以下の例ではSMTPサーバ(MTA)からのメッセージは先頭に「<」をつけて示します。SMTPクライアントのメッセージは先頭に「>」をつけて示します。
% telnet localhost smtp < 220 note-pc.example.com ESMTP Postfix (Ubuntu) > HELO localhost.example.com < 250 note-pc.example.com
挨拶が済んだらSMTPセッションのスタートです。1つのSMTPのセッションで複数のメールを送ることができます。「MAIL FROM」、「RCPT TO」、「DATA」で1つのメールを送ります。
まず、「MAIL FROM」で送信者を伝えます。
> MAIL FROM: <kou@example.com> < 250 2.1.0 Ok
次に、「RCPT TO」で宛先を伝えます。
> RCPT TO: <info@example.com> < 250 2.1.5 Ok
同じメールを複数の宛先に送ることもできます。その場合は「RCPT TO」を複数回実行します。
最後に「DATA」でメールの内容を伝えます。メールの最後は「.」だけの行になります。
> DATA < 354 End data with <CR><LF>.<CR><LF> > Subject: Hello > From: <kou@example.com> > To: <info@example.com> > > This is a test mail! > . < 250 2.0.0 Ok: queued as 054C624FB
これで、1通のメールを送信できました。続けてメールを送信する場合はまた「MAIL FROM」から始めます。
SMTPセッションを終了する場合は「QUIT」です。
> QUIT < 221 2.0.0 Bye
これで1つのSMTPセッションが終了しました。
milterプロトコルはSMTPと密接に関わっています。それでは、milterプロトコルの詳細を説明します。
milterプロトコルにもSMTPと同じようにコマンドがあります。そして、そのコマンドはSMTPのコマンドと対応したものになっています。まずSMTPのコマンドを説明したのはそのためです。
例えば、SMTPで「HELO」というコマンドが実行された場合、「HELO」に対応する「helo」というmilterプロトコルのコマンドが発行されます。このとき、SMTPクライアントが指定したHELOコマンドの引数がmilterに渡されます。
MTAはmilterにコマンドを送った後、milterからの返答があるまでSMTPクライアントには返答しません。つまり、milterが「helo」でrejectを返すことで、SMTPクライアントの「HELO」コマンドへの返答をrejectとすることができます。これにより、MTAがSMTPレベルでできることとほとんど同じことをmilterで実現できます。
mitlerプロトコルのコマンドはほとんどSMTPのコマンドに対応していますが、milterプロトコルのコマンドの方がより細かくなっています。例と一緒にコマンドの対応を説明します。
SMTPでの最初のコマンドは「HELO」ですが、milterプロトコルでは「helo」よりも前にコマンドが発行されます。それが、SMTPクライアントがSMTPサーバに接続したときに発行される「connect」コマンドです。
「connect」コマンド以外はSMTPのコマンドとmilterプロトコルのコマンドは1対1で対応します。「envfrom」の「env」は「envelope」の略で、「封筒」という意味です。「envfrom」で「差出人」という意味、「envrcpt」で「宛先」という意味です。「rcpt」は「recipient」の略で「受信者」という意味です。
SMTPでは1つのメールを複数の宛先に送信できます。この場合、複数回「RCPT TO」を指定します。STMPで複数回「RCPT TO」が指定されるので、milterプロトコルでも「envrcpt」コマンドが複数回発行されます。
SMTPの「DATA」コマンドはmilterプロトコルではより細かいコマンドに分解されています。
まず、「DATA」コマンド時にはmilterプロトコルの「data」コマンドがすぐに発行されます*1。その後、SMTPクライアントはメール本体を送信しますが、「header」などのイベントはすぐには発生しません。SMTPクライアントがデータの終了を示す「.」のみの行を入力するまでは何も起きません。「.」のみの行が入力されると、MTA側でメール本文をパースして「header」、「eoh」(end of header: ヘッダーの終わり)、「body」、「eom」(end of message: メッセージの終わり)コマンドを発行します。もちろん、ヘッダーもパースしてあるので、MTAは「ヘッダー名」と「ヘッダー値」と分解した状態で情報を渡します。
このようにmilterプロトコルはSMTPと密接に関わっています。milterプロトコルのコマンドがわかれば、自分が必要な機能を持つmilterを実現するためにはどのコマンドを利用すればよいかを考えることができるでしょう。
説明用のサンプルとしてメール検索を実現するmilterを作成します。今回はSubject、From、Toと本文のみを扱うことにします。
メール検索を実現するために、全文検索エンジンとしてgroongaを、milterライブラリとしてmilter managerのRubyバインディングを使います。
groongaは全文検索のためのインデックス作成機能だけではなく、データストアの機能も持っています。groongaのデータストアはカラム指向で、リレーショナルデータベースとは違い、レコード(行)毎にデータをまとめて持つのではなく、カラム(列)毎にデータをまとめて持っています。
このようにデータを持つと、同じカラムの複数の値へのアクセスを高速に行うことができます。このため、カラムの値を使った集計処理を高速に実行できます。集計処理とは、例えば、SQLでいうGROUP BYのような処理です。
集計処理を用いると絞り込み検索をしやすいユーザーインターフェイスを提供することができます。例えば、ショッピングサイトで商品に複数のタグがついているとします。このとき、同じタグがついている商品が何項目あるかを表示してリンクにします。1つも商品が属していないタグは表示しないようにすれば、ユーザは無駄な絞り込み操作を行わずにすみます。
全商品(123件) タグ スポーツ(58件)← リンクにする 映画(45件) ← リンクにする 食べ物(36件) ← リンクにする 旅行(0件) ← 表示しない
この状態で「スポーツ」をクリックしたとします。
全商品(123件) > スポーツ(58件) タグ スポーツ ← 選択済みなので表示しない 映画(26件) ← リンクにする 食べ物(0件) ← 表示しない 旅行(0件) ← 表示しない
このように、絞り込んだ後にがっかりするような操作を示さないことにより、絞り込み検索をしやすいユーザインターフェイスを作ることができます。がっかりするような操作かどうかを判断するために、同じ値を持つレコードの個数を数える、といった集計処理をしています。
groongaはキー管理のためのデータ構造としてハッシュテーブルとバイナリパトリシアトライを採用しています。バイナリパトリシアトライはパトリシアトライの一種です。
ここにB+木とパトリシアトライの説明を書く予定でしたが、もう、だいぶ長くなっているので省略します。また別の機会があれば紹介します。
パトリシアトライを利用すると効率よく最長一致検索を実現できます。これを試してみるためのサンプルアプリケーションを用意しました。
リンク先ではキーワードを変えて試すことができます。
最長一致機能を利用してキーワード検出している部分のソースは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 |
target_text = "..." keywords = request["keywords"].split words = Groonga::PatriciaTrie.create(:key_type => "ShortText", :key_normalize => true) keywords.each do |keyword| words.add(keyword) end tagged_text = words.tag_keys(target_text) do |record, word| "<span class='keyword'>#{word}</span>" end |
まず、パトリシアトライを作り、キーワードを登録します。Groonga::PatriciaTrie
にはtag_keys
という便利メソッドがあり、これを使うと「最長一致検索」→「キーワードにタグ付け」をより簡潔に記述することができます。
全体のソースはリンク先にあるソース一式の中に含まれています。
groongaのRubyバインディングであるRuby/groongaはスキーマ定義のためのDSLを提供しています。
メールを保存するMessages
テーブルにはsubject
、from
、to
、body
カラムを定義しています。今回は簡単のため、宛先は1つのみ扱うことにしています。
次に、高速に全文検索を行うために索引を作成します。Terms
テーブルのキーに単語(ここではbigramを利用しているので1文字か2文字の文字列)、カラムにその単語が出現するMessages
レコードのID(とN-gramなので単語の出現位置)を保持します。
subject
カラムとbody
カラムでそれぞれに対して索引を作成しています。こうすることにより、「どこかに○○が含まれているメールを検索」といった検索だけではなく、「Subjectに○○が含まれているメールを検索」、「本文に○○が含まれているメールを検索」というような細かい検索ができるようになります。細かい検索が必要ない場合はMessages
テーブルに検索対象をすべて入れたカラムを1つ作り、そのカラムに対して索引を作成してもよいでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
Groonga::Schema.define do |schema| schema.create_table("Messages") do |table| ... table.text("text") end schema.create_table("Terms", :type => :patricia_trie, :default_tokenizer => "TokenBigram", :key_normalize => true) do |table| table.index("text") end end messages = Groonga["Messages"] from = "kou@clear-code.com" to = "info@clear-code.com" body = "Hello Ruby and milter!" text = "#{from} #{to} #{body}" # <- textに検索対象をまとめる messages.add(:from => from :to => to, :body => body, :text => text) query = "Ruby" messages.select do |record| record["text"].match(query) # <- textカラムで全文検索 end |
データの保存・検索の仕組みはできたので、あとは、groongaのデータベースにメールを登録するだけです。
milter managerのRubyバインディングのAPIでは、ユーザがmilterプロトコルのコマンドに対応するメソッドを定義し、ライブラリ側がそのメソッドを呼び出します。今回必要な情報はヘッダーと本文にあります。そのため、今回のmilterは以下のようになります*2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class ArchiveMilter < Milter::ClientSession def initialize @messages = Groonga["Messages"] @values = {} @encoding = nil @body = "" end def header(context, name, value) case name when /\A(Subject|From|To)\z/i key = $1.to_s.downcase utf8_value = NKF.nkf("-w", value) @values[key] = utf8_value when /\AContent-Transfer-Encoding\z/i @encoding = value end end def body(context, chunk) @body << chunk end def end_of_message(context) nkf_option = "-w" nkf_option << " -MB" if @encoding == "base64" @values["body"] = NKF.nkf(nkf_option, @body) @messages.add(@values) end end |
このように、Rubyでmilterを作るときは必要な処理の部分だけを記述するだけですみます。つまり、やりたいことを実現するためにどういうデータが必要で、どのタイミングでそのデータを手に入れられるかがわかれば、Rubyでmilterを作ることは簡単だということです。
登録したメールは以下のように検索・表示することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
query = "Ruby" # <- 検索キーワード messages = Groonga["Messages"] result = messages.select do |record| record["subject"].match(query) | record["body"].match(query) end result.sort([["_score", :desc]]).each do |message| puts "-" * 78 puts "score: #{message.score}" puts "Subject: #{message.subject}" puts "From: #{message.from}" puts "To: #{message.to}" puts puts message.body puts "-" * 78 end |
Rubyでmilterを作る方法について説明しました。そのために必要な技術として、milterプロトコルの具体的な動作も説明しました。ここで説明されている内容を理解していれば、より詳細なmilter関連情報も理解しやすくなるでしょう。英語ですが、milterに関する情報はmilter.orgにまとまっています。より詳しい情報を知りたい場合はチェックするとよいでしょう。
札幌はやはりやさいい雰囲気に包まれていました。札幌Ruby会議02とは少し違う雰囲気でしたが、似ているとは感じました。
一度、札幌の人たちに会いに行ってみてはいかがでしょうか。
2010-02-14 - iakioの日記 - postgresqlグループ
「C言語でPostgreSQLを拡張する」というタイトルで石田さんが淡々とライブコーディングされていました。会場とやりとりをしながらコーディングする様子を見ていると、札幌っぽい雰囲気を感じることができるでしょう。
*1 「data」コマンドはmilterプロトコルのバージョン4から追加されたコマンドなのでそれより古い2などを使っている場合は利用できません
*2 milter managerでは「eom」というような省略した名前を「end_of_message」という省略しない自己記述的な名前になっているので注意してください。