lighttpdでモジュールを書く際に気をつけること

lighttpdのモジュールを書くことを覚えてしまったせいで、
ついついlighttpdのモジュールで仕事を進めてしまうクセがつきました。
「なんでもおもっど」状態です。


全パスに対するアクセス数をTokyoCabinetに記録したり、
特定のパスに対してはmemcachedから値を取得したり、
Sennaで特定のキーワードにリンクを付与したり、
まあ、やりたい放題です。パフォーマンスも出てます。
ああ毛が立っちゃう。


lighttpdのbufferやarrayを使えば、
メモリリークなどに悩まされることはほぼありません。
意外と安定して開発・稼動できたので正直ビックリしています。
まあ、cookieをパースしたりする便利関数がないので、
そこらへんは根性で書く必要はあります。
どうにかしてくれよ(そろそろ飽きてきた)。


lighttpdのモジュールの欠点は、
ビルドシステムがちゃんとしていないという点です。

上のサイトを見ると、/srcにモジュールのソースコードを入れて
Makefile.amを書き換えろ、みたいなことが書いてあると思いますが、
あれだけ入れるんじゃねえよお、という話ですね。


lighttpd本体とlighttpdモジュールを切り離して開発する場合には、
以下の点に気をつけようぜ。

  • lighttpdのソースディレクトリの/と/srcをincludeしないといけません。
  • lighttpdはconfigureしておく必要があります。
  • 通常lighttpdはoff_tが64bitという前提でconfigureされます。

モジュールのoff_tも64bitにしておかないと、
情報をやりとりする構造体イメージと変数マッピングがズレて泣きます。


あと、lighttpdtracサイトは常時重めなので、
ローカルに保存して参照するとよいです。

検索エンジンの歴史的意義と未来

弊社森(オラの上司)が大阪市立大学で発表をしてきました。
発表資料は以下からどぞー。


発表の前半は、コミュニケーション技術の歴史を踏まえ、
多対1のコミュニケーション技術として検索エンジンを捉える試みです。
発表の後半は、コミュニケーション技術としての検索エンジン
現在どのような課題を抱えているか、
本来検索エンジンとはどうあるべきか、について語られています。


発表者の森と、
検索エンジンは情報を減らすための技術だから面白い」と
盛り上がって話した記憶があります。
(ちばてつやの看板がある渋谷の中華料理店で)
大変面白い資料ですので、是非一読をオススメいたします。

Flexでキーボードイベントを取得するのに詰まる

Flex 3でコントロールのfocus関係なしにキーボードの入力を取得するには、
http://livedocs.adobe.com/flex/3_jp/html/help.html?content=events_11.html
http://www.adobe.com/cfusion/communityengine/index.cfm?event=showdetails&productId=2&postId=304
の2つの方法があります。


前者は、applicationに対してaddEventListenerする方法。
後者は、stageに対してaddEventListenerする方法。
stageはcreationComplete時にはセットされておらず、
applicationComplete時には取得できるので注意。


…といいつつ、以下のようなコードでイベントが取れなくて
2時間くらい悩んだ。

...
  application.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);
...
private function keyDownHandler(evt:KeyboardEvent):void {
  Alert.show('' + evt.keyCode);
}

原因は、keyDownHandlerってものが普通にUIComponentにもあって、
そっちを参照しに行っていたから。
ギギギギギギギ

[xml][libxml2][c]XMLをHTTPで取得して、XPathで指定された中身をC言語で取り出す方法

タイトル通り。必要に迫られて…
libxml2はAPIの数がオニのようにあります。泣きそうです。

注意:ルートノードが1つの文書しか対応していません。

根性不足で、URLからxmlDocPtrを得られるAPIを見つけられなかったため、
xmlTextReaderPtrで最初のノードを読み、
xmlTextReaderExpandで強引に子ノードを取ってこさせています。


大変ダサいので、libxml2ウィザードの方の降臨を願う。

gcc -o test -I/usr/include/libxml2 test.c -lxml2
#include <stdio.h>
#include <libxml/xpath.h>
#include <libxml/xmlreader.h>

#define URL "http://www.nicovideo.jp/api/getthumbinfo/sm9"
#define XPATH "/nicovideo_thumb_response[@status='ok']/thumb/title/text()"

int
main(int argc, char *argv[])
{
  int ret;
  xmlDocPtr doc;
  xmlNodeSetPtr nodes;
  xmlXPathContextPtr ctx;
  xmlTextReaderPtr reader;
  xmlXPathObjectPtr xpobj;

  if ((reader = xmlNewTextReaderFilename(URL))) {
    // FIXME: now only one root node is supported
    ret = xmlTextReaderRead(reader);
    xmlTextReaderExpand(reader);
    if ((doc = xmlTextReaderCurrentDoc(reader))) {
      if ((ctx = xmlXPathNewContext(doc))) {
        if ((xpobj = xmlXPathEvalExpression(
                       (xmlChar *)XPATH, ctx))) {
          if (!xmlXPathNodeSetIsEmpty(xpobj->nodesetval)) {
            xmlNodePtr node = xmlXPathNodeSetItem(xpobj->nodesetval, 0);
            if (node->content) {
              printf("%s\n", node->content);
            }
          }
          xmlXPathFreeObject(xpobj);
        }
        xmlXPathFreeContext(ctx);
      }
      xmlFreeDoc(doc);
    }
    xmlFreeTextReader(reader);
  }
  xmlCleanupParser();

  return 0;
}

x86_64環境でRubyからMySQLのクエリを実行するときの問題が示す根本的な問題…

ニコニコ大百科というサービスをリリースしたわけですが、
開発言語を選定する際に
「最近書いてなくて忘れかけてるし、部下も書けるし、
 たまにはRubyで書いてみようじゃないか。」
とテキトーに決めたことをちょっと後悔。


特にRubybase64に関しては

  1. マニュアルの使用方法の項目にはencode64などの関数を直に使う方法が書いてあるが、生で使うと怒られる(encode64 is deprecated; use Base64.encode64 instead)。
  2. Base64.encode64()を使うと、今度は途中とお尻に勝手に改行が入る。マニュアルには書いていない挙動(るびまには書いてあるが)。Base64.encode64().split.joinなどをして改行を除去する必要がある。
  3. さらに、urlsafeなエンコードをしようとすると、Base64.encode64().split.join.tr('+/', '-_')とする必要がある。

と正直ちょっとイラっとした。
Pythonだとurlsafe_b64encode()という関数がある。これはこれでやりすぎ感はあるけど、実用的。
その他にもCGIモジュール周りも結構手を入れたりして、
足周りを確保する作業を多く行いました。


Ruby、ロジックは非常に書きやすい言語なんだけどなー。
クラスの拡張なども非常にキレイかつ書きやすく出来てしまうので、
手元で問題を修正して満足しちゃって、
本家までパッチが上がらないのかもしれない。
ツッコミビリティの問題かなあ。


んで、そんなのはどうでもいいんだけど、
先日、サービスをちょっと高級なサーバにお引越ししたのです。
その際に、以下の擬似コードが動かなくてかなりあせった。

require 'mysql'
require 'pp'
my = Mysql.connect(host, name, pass, db, port)
st = my.prepare('SELECT ?')
st.execute(0xffffffff)
pp st.fetch

原因は、サーバのOSがi386版からx86_64版に変わったから。


x86_64版Rubyでは、0xffffffffはFixNumになります(irbで0xffffffff.classを見てみよう)。
基本、sizeof(long)*8-1ビットに収まる符号付数値の場合はFixNumになるみたいですね。
MySQL/Rubyのst_execute関数内で、渡されたパラメータの型によって分岐する部分があります。
FixNumなので、case T_FIXNUMのところにコードが遷移するわけです。
んで、そこでFIX2INTを呼んでいるんですねー。
sizeof(int)は4なので、符号を入れると1ビット足りない。
よって、integer 4294967295 too big to convert to `int' と怒られてしまうのだ。


この問題、ip2longした数値をカラムに入れるときに発覚しました。
パッチを書きたいところですが、
他のサービス修正で手が回らない…どこかにパッチ落ちてないかなあ。
FIX2INTをFIX2LONGにすればいいのかな…


んで、バグは直せば済むのでよいのです!
このようなすぐに見つかってもよさそうなバグが
MySQLとの接続モジュールに残っているということで、
「本当にみんなサーバサイドでRubyを使っているんだろうか」
とちょっと不安になったりしたのでした…
x86_64版のOSは今や珍しくなくなってきましたし…

追記

パッチ書いたお。
BigNumで64ビットフルに使っている場合に、is_unsignedを立てるとかいう処理はしていないけどね。
tmtmさんにもメールしよ→お返事きた。対応いただけるみたい。ヤッター!

--- mysql.c.in.orig     2008-06-03 14:28:44.000000000 +0900
+++ mysql.c.in  2008-06-03 14:31:16.000000000 +0900
@@ -1367,14 +1367,21 @@
     if (argc > 0) {
         memset(s->param.bind, 0, sizeof(*(s->param.bind))*argc);
         for (i = 0; i < argc; i++) {
+            long num;
             switch (TYPE(argv[i])) {
             case T_NIL:
                 s->param.bind[i].buffer_type = MYSQL_TYPE_NULL;
                 break;
             case T_FIXNUM:
-                s->param.bind[i].buffer_type = MYSQL_TYPE_LONG;
                 s->param.bind[i].buffer = &(s->param.buffer[i]);
-                *(int*)(s->param.bind[i].buffer) = FIX2INT(argv[i]);
+                num = FIX2LONG(argv[i]);
+                if (num <= INT_MAX && num >= INT_MIN) {
+                    s->param.bind[i].buffer_type = MYSQL_TYPE_LONG;
+                    *(int*)(s->param.bind[i].buffer) = (int)num;
+                } else {
+                    s->param.bind[i].buffer_type = MYSQL_TYPE_LONGLONG;
+                    *(LONG_LONG*)(s->param.bind[i].buffer) = num;
+                }
                 break;
             case T_BIGNUM:
                 s->param.bind[i].buffer_type = MYSQL_TYPE_LONGLONG;

ツッコめ!全文検索エンジンSennaの新しいAPIについての素案。

みんな、[Senna-dev 884]のメールは読んだかな!?
全文検索エンジンSennaの新しいAPIについての素案がついに公開されました。


今までのAPIのほとんどを刷新するという
大改造!劇的ビフォーアフターAPI群です(APIの匠)。


んで、この新しいAPI群で何ができるようになるのでしょうか。
簡単に言うと、
Sennaは、データベースになります!」


といいつつも、全文検索機能を充実させるために、
データベース的機能を強化した、という感じになっておりますぞ。

データベース機能

新しいSennaでは、複数のテーブルというものを持つことができます。
テーブルは、複数のレコードを持つことができます。
レコードは、複数のカラムを持つことができます。
カラムは、ある型のデータを保持します。
一般的なRDBMSの2次元表をまずはイメージしてください。

ポイント1. レコードごとにカラムが追加できる

Sennaの新しいAPI群では、レコード毎にカラムを追加することができます。
というわけで、二次元表ではなく、穴あきの二次元表をイメージするとよいでしょうか。

もちろん、あるカラムでの絞込みやソート、グループ化などの操作も行うことができます。

ポイント2. 外部参照が高速

いわゆる外部参照が高速に行える実装となっております。
具体的にいうと、カラムに保持する型として、他のテーブルそのものを指定することができます。
このように指定されたカラムは、他のテーブルへの参照を保持します。

ポイント3. 強力なフック

テーブル・カラムを参照もしくは更新するタイミングで、
任意の関数を呼び出すことができます。
トリガーのようなものです。
この関数で、値をフィルタしたり、値を横取りしたり、
そもそもイベントそのものを握りつぶしたりすることができます。


以上の3つのポイントが大きな特徴となっております。


それ以外にも、テーブルのタイプが3つあったり、
テーブルを物理ファイルにマップするか、メモリ上だけに作るか選べたり…
などいろいろ特徴がありますが、
大雑把に説明するとこんな感じです。

全文検索インデックスの拡張

今までのSennaでは、
主にsen_indexという型についての操作を行うことによって、
全文検索インデックスへの操作を行っていました。


sen_indexは、内部的には以下の3つの構造から成り立っています。


文書ID表は、Senna内部での文書ID番号と、外部での文書IDとの対応付けをするための表です。
外部での文書IDの実例として、
TritonnであればMyISAMのレコード位置、
Ludiaであればtuple idが挙げられます。


語彙表とは、文書内に登場する単語がすべて登録され、それぞれに語彙ID番号が対応付けされている表です。
N-gramを語彙とするのであれば、1または2文字の文字列が語彙表に登録され、
MeCabで切り分けられた形態素を語彙とするのであれば、形態素が語彙表に登録されます。


転置インデックスとは、
語彙ID番号ごとに検索データを保持するデータ構造です。
ある語彙を含む文書について、Senna内部での文書ID番号の列を語彙ごとに保存しています。
(本当はもうちょっと情報を保持しているのですが、説明の簡単化のため省略)


この3つの構造を用いることによって、全文検索を行うことができます。
具体的には、
「検索キーワード」→
「検索キーワードの語彙ID番号」→
Senna内部での文書ID番号の列」→
「外部での文書ID番号の列」
のように順番に表を引くことにより、検索結果の文書ID群を得ることができるわけです。


Sennaの新APIでは、ID表と語彙表について、Sennaデータベースのテーブルを用いることができます。
転置インデックスのみ、inv index columnという特殊なカラムが用意されています。


ID表と語彙表が一般的なテーブルとなったということは、
それぞれのレコードに好きなカラムを追加できるということです。


ID表にカラムを追加することにより、こんなことが出来るようになるでしょう。

  • 日付カラムを追加して、その日付での高速な絞込み・ソート・グループ化
  • 無効化フラグを追加して、検索結果から即時に除外をする

…つまり、

  • 文書ごとに複数の属性値が保存できる
  • 属性値で絞込み・ソート・グループ化ができたり、フックを用いてさまざまな操作を行ったりできる。

の2つの併せ技で出来ることが増えるというわけなのです。


語彙表にカラムを追加することによって、こんなことが出来るようになるでしょう。

  • query式を用いた場合でもTF-IDFによるスコア計算を可能に
  • 検索ストップワードなどの実現
  • ある語彙に関してのスコアだけを上下する操作
  • 文書登録日別の語彙の総出現数をカラムに追加して、バズワードの検出

…つまり、

  • 語彙ごとに複数の属性値が保存できる
  • 属性値で絞込み・ソート・グループ化ができたり、フックを用いてさまざまな操作を行ったりできる。

の2つの併せ技で出来ることが増えるというわけなのです。


夢が広がりまくりんぐですねー。

悩み深きAPI設計…

弊社SennaチームはAPIの設計に悩んでいるのです。
大体、こんなことで悩んでおります。

  • パフォーマンスが発揮できるように…
  • 実用的なように…
  • いろんな言語でバインディングを書きやすいように…
  • API仕様が安定するように…


というわけで、

  • このAPIじゃこういうアプリケーションが書きにくい
  • このAPIじゃこういう言語だとバインディングが書きにくい
  • そもそもこういう機能が欲しい

などというツッコミを大募集しております。
senna-devメーリングリストやブログなどで、
是非是非ツッコみまくってください!!!
高速で実用的で柔軟で安定した検索エンジンを目指してるんじゃい!!


というわけで、お待ちしぃ〜てぇ〜いまぁ〜すぅ〜 チャリラリラリラリラン(by 日本直販

ニコニコ大百科リリース

ニコニコ大百科という、ニコニコ動画Wikipediaとも説明すべきサイトをリリースしました。

5/25 25:25:25公開という設定になっております。
社会人になってから、初めての外部向けWebサイト構築がこれだよ!


ミドルウェア開発が本業(?)なので、
ミドルウェアばっかりに凝ってしまいました…あはは…


他にも、各ページの閲覧数ランキングのために、Tokyo Tyrantを投入予定です。