非同期で全文検索エンジンgroongaを叩く AnyEvent::Groonga 書いたよ

要するにAnyEventでgroongaを使いたかったのでperlモジュール書きました。んでもって久々にCPANにアップしましたよ、という告白です。

AnyEvent::Groonga - Groonga client for AnyEvent
http://search.cpan.org/~miki/AnyEvent-Groonga/

非同期でガンガン全文検索エンジンを叩きたいな、ということでAnyEvent::Groonga。

なおYappo さんが取り組まれているCライブラリのperlバインディングスとは異なり、AE::Groongaはgroongaディストリビューションに同梱されてるオリジナルの「groongaサーバ」を対象としています。

このgroongaの組み込みサーバは、じつはhttpとgqtp(groonga独自プロトコルらしい)両方をしゃべれます。また普通にローカルのDBとしての問い合わせもできます。

んで、このgqtpプロトコルとローカルDB問い合わせについては、どちらの場合にもクライアント側にgroongaがインストールされている必要があります。

ですがhttpであればクライアント側にはgroongaは必要ありません。なのでgroongaサーバ以外のマシンからは普通にhttp経由で呼び出すのがお手軽なユースケースかと思われます。

使い方

使い方はこう。

use AnyEvent::Groonga;

my $groonga = AnyEvent::Groonga->new(
    protocol => 'http',
    host       => 'localhost',
    port       => '10041',
);

# blocking interface
my $result = $groonga->call(
    select => {
        table                  => "Site",
        query                 => 'title:@test',
        output_columns => [qw(_id _score title)],
        sortby                => '_score',
    }
)->recv;

print $result->dump;

$any_event_cv = $groonga->call( groongaコマンド => 引数リファンレンス ) という形になります。

AnyEventのcondvarがかえってくるので、そのままrecvすればブロッキング風になります。
ノンブロッキングにする場合はcall backを指定してください。

# non-blocking interface
$groonga->call(
    table_create => {
        name => "Test",
        ....
    }
)->cb(
    sub { 
        $result = $_[0]->recv;
    }
);

なお$resultとして取得したオブジェクトですが、AnyEvent::Groonga::Resultというクラスのインスタンスになっています。

例外としてselectコマンドの時だけAnyEvent::Groonga::Result::Selectというさらに深いネームスペースのオブジェクトを返すようにしています。

selectの時だけ$result->hit_numとか$result->itemsなどのメソッドが使えてちょっと便利、にしたつもりなんですが、どうかな。。自分でももっと使い込んでみないとわからんな。ここら辺の設計はだいぶ甘いかも。

なおgroongaのコマンドは全部で20種類以上あり、一応その全部に対してAnyEvent経由でコマンドを投げられるはずなんですが、まだ検証はたりてません。

元のDSLが少々むずかしくて、とくにJSONパラメータでのクオーテーションのエスケープとか、文字コードまわりとか、泥臭い変換処理を伴うため、まだまだ不十分。バグとか不具合などあったら教えてください。

なおgroongaコマンドの詳細についてはgroongaのオフィシャルサイトのドキュメントを参照して下さい。

AnyEvent::Groongaの使い方は、まだあんまりドキュメント書けてないのでテストコードでも見ながらがんばってみてください。

sennaとgroongaについて

全文検索エンジンと言えばsenna、ということで今まで頑にsenna(正確にはtritonn)だけを愛し続けてきたのですが、やっぱり書き込みが多くなってくるとMyISAMのテーブルロックが頻発し、全体のパフォーマンスが低下するし。。。そろそろお別れかしら、と思い始めていました。

というわけで、そろそろgroonga!

  • なんといっても「参照ロックフリー」だゼ
  • mysqlやpostgresに依存しない独自のストレージ持ってるゼ
  • カラム志向のちょっと頑固なKVSだゼ

もうgroongaがまぶしすぎて困ります。

実際に書き込み処理をループで大量にまわしつつ、同じディスクを読んでいるgroongaサーバにselectを並列して投げてみましたが、ほとんどパフォーマンスは劣化しません。参照ロックフリーってすごくいいかも〜。

mysqlのgroongaストレージエンジンも既にリリースはされていますが、
結論として、groongaオリジナルサーバでも十分にパフォーマンスよさげなので、AnyEvent::Groongaは手っ取り早くgroongaの恩恵にあずかりたい、とう人におすすめです。

perlでデーモン書く時は素直にMoose化しておくのもいいかもな

今更Mooseの話題かよ、と思われることでしょうが、自分は常に流行の3年遅れぐらいを全力で追いかけるタイプなので、自分にとっては今が旬。というわけで、Moose的なはなしを書きます。

突然ですが、現役バリバリのperl使いのみなさんに質問です。

POEやらAnyEventやらでちょっとしたアプリケーションサーバを書く場合、みなさんはどうやって「デーモン化」してますか?

自分はもう何回もこの手のものを書いてきたつもりですが、実は未だにベストな手法を編み出せてません。。。

App::DaemonDaemon::Genericあたりでなんとなく自分流な形を模索した時期もありましたが、結局どれも面倒くさくなって、最近では「もう nohup perl hoge_server.pl & でいいじゃん」みたいな。「止める時は pkill -f hoge_server でいいじゃん、文句アッカ」みたいな。そんなダメダメな運用を繰り返す今日この頃であります。

daemontoolsじゃないヤツで

そんな話をすると「最近はdaemontoolsがいいみたいよ」という流れになるわけですが、自分はどうもあの雰囲気になじめないんですよ。

「/serviceとか大胆な配置場所がちょっと鼻につくぜ」とか「みんながイイっていってるのは本当はイクないんだぜ。」とか訳の分からない理由をつけては新しいものの導入を拒否してしまいます。

新しいものを拒否するという現象はいよいよエンジニアとしての終わりが近いナ、と自分でも感じる今日この頃なんですが、流行の3年あとを追いかけるオールドタイプにとっては新しすぎるのは無理なんです。はい。

まぁ本当はただの食わず嫌いなんですが、とにかくdaemontoolsはちょっと矢田。できれば外部ツールにたよらずperlだけでシンプルに解決したいな、と。

そこで考えてみたんですが、MooseX::DaemonizeとMooseX::LogDispatch、あとMooseX::SimpleConfigをうまく組み合わせたものでデーモン化の下地をつくってやって、そこにアプリケーションサーバ本体を載せるような構成はどうだろうか、と。

構成イメージ

イメージはこんな感じです。
libの下にAppというネームスペースをつくってあげて、その中で「MooseXを組み合わせたデーモン化の下地」を書いておきます。そしてscript/my_server.plからはApp::MyServerを、App::MyServerからはMyserverを、という順に呼び出します。

.
|-- lib
|   |-- App
|   |   `-- MyServer.pm
|   |-- MyServer
|   |   |-- Fuga.pm
|   |   `-- Hoge.pm
|   `-- MyServer.pm
`-- script
    `-- my_server.pl

つまりデーモン化の足回り的なコードはすべてApp::MyServerに閉じ込めてしまします。ここでMooseXをモリモリ使います。それ以外のサーバ本体のコードとはきれいに分離できるようにしています。

実はこのアイデア、牧さんが2年くらい前に書いていたHamakiを見て盗作させていただきました。ゴチです。

で、そのデーモン化の足回りとなるApp::Myserverのコードはこんな感じになります。
適当に書いているのでがちゃがちゃしてますが。

package App::MyServer
use Moose;
use MyServer;

with qw(MooseX::Daemonize MooseX::LogDispatch MooseX::SimpleConfig);

has '+configfile' => ( default => "/etc/daemon.yaml", );

has config => (
    is       => 'ro',
    isa      => 'HashRef',
    required => 1,
);

has log_dispatch_conf => (
    is       => 'ro',
    isa      => 'HashRef',
    lazy     => 1,
    required => 1,
    default  => sub {
        my $self = shift;
        if ( $self->foreground ) {
            {   class       => 'Log::Dispatch::Screen',
                min_level   => 'debug',
                stderr      => 1,
                DatePattern => 'yyyy-MM-dd',
                format      => '[%d] [%p] %m at %F line %L%n'
            };
        }
        else {
            {   class       => 'Log::Dispatch::FileRotate',
                min_level   => 'debug',
                filename    => '/tmp/app.log',
                mode        => 'append',
                DatePattern => 'yyyy-MM-dd',
                max         => 50,
                format      => '[%d] [%p] %m at %F line %L%n'
            };
        }
    },
);

has pidbase => (
    is      => 'rw',
    isa     => 'Str',
    default => "/tmp/",
);

after "start" => sub {
    my $self = shift;
    return unless $self->is_daemon();
    my $server = MyServer->new(
        config => $self->config,
        logger => $self->logger,
    );
    try {
        $server->start;
    }
    catch {
        warn "an error occuerd: $_";
    }
};

no Moose;

__PACKAGE__->meta->make_immutable;

1

ちなみにこっちが起動スクリプト

# my_server.pl

#!/usr/local/bin/perl -w
use strict;
use warnings;
use App::MyServer;

my $daemon = App::MyServer->new_with_options();

my ($command) = @{ $daemon->extra_argv };
$daemon->$command if $daemon->can($command);

warn( $daemon->status_message );
exit( $daemon->exit_code );

まず言えることとして、MooseX::Daemonizeのおかげでstart, stop, restartなどが使えるようになっています。

またMooseX::Daemonizeは内部でMooseX::Getoptを使ってるので、これだけで引数の処理を面倒みてくれるしMooseX::SimpleConfigとの食い合わせもよいようで、configファイルでの指定と引数での指定をミックスして使うことも可能になります。

# 起動イメージ
perl -Mblib script/my_server.pl --configfile ./config.yaml start

さらにさらに、MooseX::Daemonizeの機能で -f オプションをつけるとフォアグラウンド処理になるので、MooseX::LogDispatchもこれに連動するようにしてます。-f をつけて起動すると自動的にスクリーンに、そうでない時はapp.logというファイルにロギングする、とかね。気が利いてるでしょ。

あとは$configと$loggerをオブジェクトとしてそのままサーバ本体のコンストラクタに渡している点もこだわりポイントです。

こうすることで、サーバ本体側では$loggerや$configをそのまま使えばよい。
つまり今まで動かしていたようなサーバプログラムに最低限の改修をくわえるだけでこのウツワの上に載せることができるかなと。無理に全体をMoose化しなくてもよい、という状態にしたかったのです。

まとめ

自分は今までかたくなに「Mooseは嫌いだ」と言ってきました。自分とこの開発チームでのMoose導入もガッチリ阻止してきました。

なのでこのブログを読んでくれる仕事関係の方々は「はぁ?このおっさん今更なに言ってんのさ!」と思うことでしょう。


ごめんね。B型だから言うことがコロコロ変わるのさ。勘弁してつかあさい。




いや、正直いうと、今でも「何でもかんでもMooseを導入するのはよろしくない」と思ってます。

特に起動にかかる時間は間違いなく長くなるので、何度も呼び出すようなプログラムはMooseを使わない方が良いと思います。

でもサーバスクリプトなんかは起動に少しくらい時間がかかってもたいして問題にならないし、部分的にMoose化するのは悪くないかなと。MooseXなRoleたちを上手につかってやると自分流の定番サーバスタイルができあがるかな、と思った次第です。

いま仕事でアプリケーションサーバを再構築しているものがあるので、まずそいつを上記のような構成にして試してみようかなと考えてます。


おわり。

「みんなの検索」リリースしました

昨日、仕事で開発をすすめていた検索機能をリリースしたので、ちょっと紹介します。

「gooウェブ検索」で、自分に似た検索をしている人たちの関心事が見える機能「みんなの検索」を提供開始

gooのweb検索で適当なキーワードで検索すると、結果表示面の一番したの方に「みんなが検索中」というボックスが表示されます。

これは何かというと、いま入力したキーワードと同じような意味や関心ごとを持って検索している人たちの検索キーワードがストリームっぽく表示される、というモノです。

http://search.goo.ne.jp/option/topics/2011/

「みんなの検索」は、同じまたは類似した検索キーワードを入力して検索している他のユーザが、どんな検索キーワードを入力しているのかを表示する機能です。あなたに似た人たちが、たった今しらべているキーワードのストリームが表示されます。

これまでの関連ワードやサジェスト機能とは異なり、「知りたかった」ものを見つけやすくするだけでなく、あなた自身も気づいていない「知りたい」欲求を刺激するキーワードを提供します。

コンセプトのようなもの

いままでの検索は、とにかく目的の情報に最短距離でスピーディーにたどり着くための道具でした。

それに対して、「みんなの検索」でねらっていることは「脱線」です。

例えば、町中をぶらぶら歩いているときって、周りの人の様子や景色が自然と目に入っていると思います。無意識のうちに。「あの子がきてる服かわいいな」とか「あ、こんな店できたんだ」とか。

それって人間という動物が、無意識のうちにさまざまな「気づき」を得るためにすごく高度な「情報探索」を行っている、ということだと思うんですよ。

その観点からすると、一般的な検索エンジンが提供している機能とは、いってみればその高度な人間さまに競走馬のマスクをつけさせて、「目的物しか見せない」「とにかくよそ見をせずに、速く検索結果にたどりきなさい」といっているようなモノかもしれません。

事実、調べたい情報や知りたいことが明確に定まっているときには「速さ」というのが最も価値が高くて、それが多くの人に支持されているわけですが、せっかくの高度な頭脳と尽きることのない知的欲求をもった「人間さま」に対して、情報を絞り込むだけのアプローチしか提供しない、というのは面白くない。というかもったいない。

なので「みんなの検索」では自分と近しい興味・関心ごとを持った人たちが何をしているのかを、心地よいノイズとして出していこう、と考えた訳です。

自分が調べたいと思ったことと、直接関係はないかもしれないけど、思わず「おっ」と思ってしまうような情報、もしくはその手がかりが、偶然にも目に留まるかもしれません。

脱線 = 探索行動の入り口として、検索そのものを楽しんでもらえたらいいなぁと思ってます。

技術的なはなし

えっと、あまり細かいことは書けません。

ですが、ざっくり言うと、裏側で直近数時間分の検索データを類似ユーザというくくりでインデキシングしておき、検索クエリーが投げられた瞬間にオンザフライでベクトル化して類似検索、みたいな流れです。

「類似ユーザ」ってどうやってくくってるのよ?とか
オンザフライでベクトル化ってどうすればできるのよ?

といった部分は秘密のアッコちゃんです。ナイショ。

大量のベクトルデータのインデキシングと類似検索の部分は、以前このブログで紹介したLuigi(ルイージ)を使ってます。

http://d.hatena.ne.jp/download_takeshi/20101121/1290364695
perlで高速な類似検索エンジンを構築できるようにしてみた

実験的なコードなんでCPANにあげてませんが、実戦で使いつつ、そのうち洗練してきたらアップしようかな。


というわけで、gooでの検索がちょっとだけ面白くなったのではないかと思いますので、ぜひ使ってみてください。

ブロッキングする処理を外部プロセスに任せる - AnyEvent::Workerを使ってみた

AnyEventを使う場合に、どうしてもブロッキングしてしまうような処理があるとして、それを外部プロセスとして切り出しつつ、メインのイベントループの中に取り込みたいんだよな、と。

そんな時はAnyEvent::Workerがよさそうです。

AnyEvent::Worker - Manage blocking task in external process
http://search.cpan.org/~mons/AnyEvent-Worker/

POEで言うところのPOE::Component::Genericのようなものらしいです。使いこなせるようになるといろいろと便利!

use strict;
use warnings;
use AnyEvent;
use AnyEvent::Worker;

$|++;

print _timestamp(), "開始しまーす\n";

my $cv = AnyEvent->condvar;

print _timestamp(), "AnyEvent::Workerで外部プロセス化", "\n";
my $worker = AnyEvent::Worker->new( ['My::App'] );

print _timestamp(), "ブロッキングする処理を投げる", "\n";
$worker->do(
    blocking_task => 'a happy new year!',
    sub {
        my ( $worker, $return_message ) = @_;
        print _timestamp(), "外部プロセスからかえってきたよ", "\n";
        $cv->send($return_message);
    }
);

print _timestamp(),
    "その間にメインのイベントループでは他の処理ができるんだぜ", "\n";
sleep 2;
print _timestamp(), "メインループでもわざと2秒眠ってみたフリ", "\n";

my $rec = $cv->recv;
print _timestamp(), $rec, "\n";

sub _timestamp {
    my ( $sec, $min, $hour, $day, $month, $year ) = localtime(time);
    sprintf("%02d時%02d分%02d秒\t", $hour, $min, $sec);
}

#----------

package My::App;

sub new {
    my $class = shift;
    my $self = bless {@_}, $class;
    return $self;
}

sub blocking_task {
    my $self    = shift;
    my $message = shift;

    for ( 1 .. 5 ) {
        print "\t", _timestamp(), "外部プロセスでブロッキングしてしまう処理", "\n";
        sleep 1;
    }
    return $message;
}

sub _timestamp {
    my ( $sec, $min, $hour, $day, $month, $year ) = localtime(time);
    sprintf("%02d時%02d分%02d秒\t", $hour, $min, $sec);
}


これを実行するとこうなります。
インデントして書いてある行は外部プロセスで実行した処理です。

imac:Hacks miki$ perl any_event_worker.pl 
07時55分09秒	開始しまーす
07時55分09秒	AnyEvent::Workerで外部プロセス化
07時55分09秒	ブロッキングする処理を投げる
07時55分09秒	その間にメインのイベントループでは他の処理ができるんだぜ
	07時55分09秒	外部プロセスでブロッキングしてしまう処理
	07時55分10秒	外部プロセスでブロッキングしてしまう処理
07時55分11秒	メインループでもわざと2秒眠ってみたフリ
	07時55分11秒	外部プロセスでブロッキングしてしまう処理
	07時55分12秒	外部プロセスでブロッキングしてしまう処理
	07時55分13秒	外部プロセスでブロッキングしてしまう処理
07時55分14秒	外部プロセスからかえってきたよ
07時55分14秒	a happy new year!

外部プロセスでは1秒スリープを5回まわしてます。
なので「ブロッキングする処理を投げる」から5秒経って「外部プロセスからかえってきたよ」が表示されてますね。

注目すべきはその5秒の間にメインのイベントループの中でも他の処理ができているという部分です。
これがAnyEvent::Workerの良いところですね。ふんづまる処理は外部プロセスに押し出してしまおう、という発想です。

あ、ちなみにメインのイベントループで2秒スリープしているのは演出のため、わざとです。
実際にはブロッキングするような処理をAnyEventの中で書いては意味ありませんので、そこんところヨロシク。

そして最後に「外部プロセスからかえってきたよ」「a happy new year!」ということで、
新年あけましておめでとうございます。今年もよろしくござ候ー。

perlで高速な類似検索エンジンを構築できるようにしてみた

すみません。タイトルはやや釣り気味です。

類似検索エンジンというか、そのアイデア程度の話なんですが、以前から考えていた類似検索エンジン風のネタがあったので、ちょっとperlで書いてみたので、そいつを晒してみます。

Luigi   https://github.com/miki/Luigi

類似検索なのでLuigi。ルイージとか読みたい人はそう読んじゃっても良いです。(冷)

考え方と仕組み

類似文書の検索、となりますと一般的には超高次元での空間インデックスとかが必要になります。
昔からR-TreeやSR-Treeなど、いろいろと提案されていますが、より高次元になると「次元の呪い」によりパフォーマンスが出なくなる、なんて言われていますね。
そこで最近ではLSHに代表されるような、より高度な「近似」型のインデキシング手法が人気を集めているようです。

で、今回考えたLuigiも実は近似型のインデックスです。ただし文系脳で考えたのですごく簡単&イメージしやすいと思います。

(1)まず検索対象となるデータを用意します。文書ラベルに対してその文書の特徴語がスコア付きで並んでいるようなデータです。

$data = {
    文書label => {
        単語 => score, 単語 => score, 単語 => score, ...
    }, 
    文書label => {
        単語 => score, 単語 => score, 単語 => score, ...
    }, 
    文書label => {
        単語 => score, 単語 => score, 単語 => score, ...
    }, 
}

(2)上記のデータに対して、まず非階層型クラスタリング(K-Meansなど)を実施。

(3)各クラスタの重心点とそのベクトルをピックアップし、もう1度(1)のようなデータ構造を作る

(4)(3)で作った重心点のデータに対して改めてクラスタリングを実施し、(3)に戻る

最終的に処理すべきノードがなくなるまでこれを繰り返します。
すると各ノードが「意味」としてまとめられたバランスのとれた木構造ができあがります。

検索時には検索したいキーワードをベクトル化した上で、このツリーのRootノードから投げてやります。
各ノードでは自分の子ノードの重心ベクトルとクエリーベクトルの類似度(もしくは距離)を計算し、類似度が高かった子ノードが選ばれ、されにその子供(孫)ノードへとおりていきます。最後に葉(リーフ)にたどり着いたらオシマイ。

ちょっと調べてみたところopencvのマニュアルサイトあたりで「Hierarchical K-Means Tree(階層型KMeansツリー)」なんて言葉も出てくるようなので、やってることは同じなのかもしれません。

実験結果

簡易な実験結果です。某QAサイトのデータを1万件ほど用意してインデックスを作りました。
それに対して「インフルエンザ」「不倫」「確定申告」「トヨタ自動車」と何の脈絡もなく、適当にキーワードを投げてみました。

データが1万件と少ないので、入力した言葉に大してのマッチ具合がちょっとぼんやりしてますが、それでも類似文書検索にはなっています。

なお各結果は左の数値が類似度、右の文字列がQAの文書ラベルです。
下部には[keyword expand elapsed]と[similarity search elapsed]という項目がありますが、これは経過秒数です。
keyword expand は入力されたキーワードをYahooAPIを叩いてオンザフライで特徴ベクトルに変換する処理です。なのでどうしても低速です。
similarity search の部分が実際にLuigiがインデックスツリーをたどって検索した処理の秒数となっています。

Input keyword: インフルエンザ
0.581558925433416 インフルエンザの感染経路について - 生物学 -
0.506834581525096 触れた物でインフルエンザに感染する可能性について - 病気 -
0.416235552701384 インフルエンザ 完治。 - ヘルスケア(健康管理) -
0.257363443533322 インフルエンザによる鶏の殺処分 - 自然環境問題 -
0.183202534089901 新型インフルエンザの感染者人数について - 病気 -
0.107096136233281 感染したのでしょうか。 - ウィルス対策 -
0.107073400034487 どんな症状でウイルスに汚染されたと判断できるのですか? - ウィルス対策 -
0.10196785878913 自覚症状なしで感染はある? - ウィルス対策 -
0.100320950732344 ウイルスの感染予防 - ウィルス対策 -

                                                                                                                                                                                                      • -

[keyword expand elapsed ] 1.24062
[similarity search elapsed] 0.007747

                                                                                                                                                                                                      • -

Input keyword: 不倫
0.928112763412724 不倫のはじまり? - 恋愛相談 -
0.74892012063956 不倫をする女性を理解できるのか? - 夫婦・家族 -
0.723756525050272 都合のよい女? - 恋愛相談 -
0.640222414512117 不倫 慰謝料 - 夫婦・家族 -
0.581905311603478 既婚者の人と付き合ってます・・・ - 恋愛相談 -
0.519328972624065 はじめての携帯チェック(男性の方回答お願いします) - 恋愛相談 -
0.469573317060705 夫が悪い?妻が悪い? (不倫の事・・長文です) - 恋愛相談 -
0.437586348780523 既婚者ですが恋をしてしまいました・・・ - 恋愛相談 -
0.406506723588201 この男性の心理は? - 恋愛相談 -
0.40090569692471 感受性がない(人の心がわからない)人は病気なのか、性格なのでしょうか?... - 夫婦・家族 -
0.397803793924256 妻子もちの彼に私への愛情はあるの・・・? - 恋愛相談 -
0.36090868212552 図々しい職場の女性にギャフンと言わせたい。(長文です) - 恋愛相談 -
0.357805881567324 お金のためと言え、妻が他人とハメ撮り - 夫婦・家族 -
0.330370489228686 不倫の恋愛について - 恋愛相談 -
0.295080022135181 妻又は夫の不倫 - その他(ライフ) -
0.268932663691503 別れた不倫相手が妊娠した。 - 恋愛相談 -
0.260702721742602 子供の為に離婚しないという選択 - 恋愛相談 -
0.248174912998768 奥さんの思い込み・・・ - 法律 -
0.215141243969756 自分の浮気OKの女性へ質問 - 恋愛相談 -
0.205917241884543 離婚するための準備 - 夫婦・家族 -
0.202873460208085 潮時?? - 恋愛相談 -
0.143639484514496 10年前の彼氏がまた好きになってしまいました - 夫婦・家族 -
0.136331768702082 帰化 - 法律 -
0.123197055943046 離婚後も 同居したい 親や子供の学校にばれない様にする方法 - 夫婦・家族 -
0.117817802306074 遊び(浮気)相手に買いますか? - 恋愛相談 -

                                                                                                                                                                                                      • -

[keyword expand elapsed ] 0.676104
[similarity search elapsed] 0.011468

                                                                                                                                                                                                      • -

Input keyword: 確定申告
0.448847725667931 アフィリエイト報酬の確定申告 - 税金 -
0.331799631447844 税務署の届出について - 起業 -
0.301505276386864 扶養家族について - 税金 -
0.251311917852604 国民健康保険と確定申告 - 健康保険 -
0.231278394261396 途中から加入した扶養控除は・・・? - 税金 -
0.21445531184007 消費税申告が1日遅れたら5%も加算されました - 税金 -
0.209033457089336 年金生活者の株取引での利益 - 税金 -
0.208093399571145 低い年収でも青色申告にするメリットってありますか? - 税金 -
0.201014863628649 年金と控除 - 年金 -
0.118114070134288 所得が38万円以下の場合、配偶者特別控除申告書に記載する必要は? - 税金 -

                                                                                                                                                                                                      • -

[keyword expand elapsed ] 0.509284
[similarity search elapsed] 0.006793

                                                                                                                                                                                                      • -


Input keyword: トヨタ自動車
0.33857076472299 初購入 - 国産車 -
0.290008657323579 トヨタイプサムに乗っている方と車高を落としている方へ  - 国産車 -
0.255260034426658 トヨタのパッソを購入したいと思ってます - 国産車 -
0.180490218245698 トヨタでスライドドアのあるお勧めファミリーカーを教えて下さい。 - 国産車 -
0.173544528124501 トヨタの増産で・・・ - ニュース・時事問題 -
0.158633301729542 ミニバンのランニングコスト - 国産車 -
0.123424426531673 車に詳しい方にお聞きします。 - 国産車 -
0.11085629266251 車の車種に詳しくなりたい! - その他(車) -
0.100809731019605 同車種でディーゼルからガソリン乗り換え。有利な売却は? - 国産車 -

                                                                                                                                                                                                      • -

[keyword expand elapsed ] 0.843112
[similarity search elapsed] 0.006991

                                                                                                                                                                                                      • -

実際に動かしてみるとLuigiによる類似文書検索部では0.006秒前後で答えを返しているのがわかります。

このサンプルでは1万件のデータを対象にしましたが、50万件程度に増やしてみても0.01〜0.04秒程度でした。

課題

課題はたくさんあります。

まずはLuigiという名前がダサイです。Tree::Hierarchical::KMeans だと長過ぎるので適当につけましたが、ベタベタ過ぎてなんだかちょっとハズカシイ。。。

あと実験コードなので、1つもテスト書いてないし、スクリプトの構成も書きなぐり風です。実験的なモノなので完成度はかなり低めです。

インデックスの更新もできません。今のところ更新するには作り直ししかないです。

いいところ

えっと、メモリに全部のせてるので速いです。perlで0.006秒とかってかなり速いと思います。C++で書き直したらさらにすごいかも。

あと精度がそこそこいい。
ベースとなるクラスタリングツール(Bayonを使わせてもらってます)が優秀だから、というのもありますが、実は独自に工夫しているポイントもあります。

今回のような「クラスタリング結果の積み上げ方式ツリー」に対して、単純にノードをただ辿るだけの検索を行うと、類似度の測定にあやまりが多くなります。
これは各ノードでの検索対象が「重心点」であり、各クラスタの分散具合などは考慮されていないので、結構な確率で誤ったノードへと導かれてしまいます。
しかもRootに近いところで間違うと全然違うクラスタへと導かれてしまうという致命的な罠が待っています。

そこでLuigiでは最も類似しているノード数個を並列してたどるような仕組みをとっています。
その中で生き残ったノードの子ノードたちに対しても、1つではなく複数の候補を持って辿るような考え方です。

試してみたところ、1つだけを対象にすると7〜8割前後の精度しか出ませんが、同時に2〜5個くらいを辿るようにするだけで、誤判断はほぼなくなるようです。



というわけで、興味のある人はgithubから落として使ってみてください。

行列分解ライブラリredsvdで潜在的意味インデキシングを試してみたの巻

久しぶりに自然言語処理的な話です。

すこし前にPFIの岡野原さんが公開されたredsvdを試してみました。

redsvd は行列分解を解くためのC++ライブラリであり、特異値分解(SVD)、主成分分析(PCA)、固有値分解などをサポートしています (中略) 例えば、行と列がそれぞれ10万、非零 の要素が100万からなる行列に対する上位20位までの特異値分解を1秒未満で行うことができます.

1秒未満って、す、す、すごくねぇだべか?

というわけで早速導入してみますた。

インストール

redsvdは内部の行列演算などにeigen3を使っているとのことなので、まずはこいつをセットアップ。あ、そうそうCMAKEも必要だよ。

ちなみに自分の環境でmake checkしたらエラーが少し出てたけど、気にせずそのまま突っ込んでみました。

続いてredsvdをインストール。
マニュアルサイト見ながらやれば問題ないかと思われますので、ここらへんの詳細は省略します。

下ごしらえ

さて、さっそく!
と言いたいところですが、いろいろと下ごしらえが必要です。

今回は解析対象データとして、某検索サイトの検索ログを数万件分を持って来て、この検索クエリーを関連語で適当に拡張させたものを使います。

検索クエリーを関連語に拡張させるのは、拙作のLingua::JA::Expandというperlモジュールでもできるし、YahooAPIの「関連検索ワード」を使ってもいいかもね。まぁ、データは適当に用意した、という前提ですすめましょう。

ちなみにこの時点では以下のような

「検索クエリ key value key value key value .....」

というようなフォーマットだと仮定します。
要するに検索クエリに対して関連語が bag of words として並んでいるようなフォーマットです。

実例↓
feature_vector.txt

B-1グランプリ順位 グランプリ 57.5651697754102 順位 55.2949308955951 厚木 49.9866441630489 B-1グランプリ 39.3727412201767 祭典 34.3234789476085 中間 32.5569044283475 横手 32.4415825522972 -1 29.5295559151325 投票 24.6079632626104 富士
宮 24.1869095999417 津山 23.9561674837952 八戸 21.9336671532829 久留米 20.8208354120631 発表 19.8165814990785 八戸せんべい汁 19.6863706100883 ご当地グルメ 19.6863706100883 りう 19.0337310644589 行田 16.6138645318661 開催 15.4453195644505
横手やきそば 14.7647779575663 B級グルメ 14.7647779575663 箸 13.5186705477277 グルメ 12.8579683394308 太田 12.2752941145721 日本一 12.1918731374094 初日 11.6451320212923 最終 11.3429842290652 ゴールド 10.5028675612984 優勝 10.3416080924982 三浦海岸 10.185320100199
 ・
 ・
 ・

この例だと「B-1グランプリ順位」という検索クエリに対して、それ以降で「グランプリ => 57.5651...」や「順位 => 55.294...」のように素性が並んでいます。
なお数値部分はTFIDFなどの重み付けされたスコアを使ってます。



さて、早速redsvdをぶん回してみたいのですが、このままだと食べてくれません。

疎行列の場合にはlibsvdでつかわれている表現方法と同じように各行毎に、0では無い列番号とその値をコロン区切りで書く方法で行列を表します.

むぅ。面倒ですがこんな感じのスクリプトで加工。

use strict;
use warnings;

my $word_idx;
my @labels;

while (<>) {
    chomp $_;
    my @f       = split "\t", $_;
    my $label   = shift @f;
    my %wordset = @f;

    push @labels, $label;

    my @item;
    while ( my ( $key, $value ) = each %wordset ) {
        my $idx = _idx($key);
        push @item, $idx. ':' . $value;
    }
    print join(" ", @item);
    print "\n";
}

# ラベル(検索キーワード)は別ファイルでとっておく
open(OUT, "+>", "labels.txt");
for(@labels){
    print OUT  $_,"\n";
}
close(OUT);


sub _idx {
    my $key = shift;
    $word_idx->{$key} || do {
        $word_idx->{$key} = int( keys %$word_idx ) + 1;
    };
}
       

走らせてみます。えい。

perl filter.pl feature_vector.txt > svd_in.txt

そうするとこんな感じ。

1:4.81836340644913 2:7.03558865049202 3:7.83207818255356 4:4.36772932997306 5:7.02204442538426 6:3.65583160006774 7:2.32278780031156 8:7.41858090274813 9:2.94124408826992 10:1.77431255562433 11:5.86254376704114 12:4.52082903755434 13:5.92492802330774 14:7.1665260079395 15:2.44876760317213 16:3.70365474023736 17:3.70365474023736 18:6.51841955280386 19:5.417100902538 20:6.00759392903787 21:5.63127578386945 22:6.57701371706991 23:4.81836340644913 24:3.70365474023736 25:6.1247673944224 26:7.67562600573802 27:10.1468338111679 28:7.41858090274813 29:4.66279929882473 30:1.84769510155836
 ・
 ・
 ・

ラベル(検索クエリー)がなくなって、素性のキー部分が1から始まる数値に置き換わりました。

これでredsvdのスパース行列のフォーマットになりました。

ようやく本番

さて、redsvdの登場です!

今回試してみるデータですが、24万クエリくらい。
列数は28万ぐらいのスパースマトリックス

こいつを特異値分解により200次元に圧縮してみます。

[miki@godzilla work]$ redsvd -i svd_in.txt -o svd_out -r 200 -f sparse
compute SVD
read matrix from svd_in.txt ... 12.0844 sec.
rows: 281280
cols: 243421
rank: 200
compute ... 148.231 sec.
write svd_out.U
write svd_out.S
write svd_out.V
50.6651 sec.
finished.
[miki@godzilla work]$

終わりました。
データの読み込みに12秒、計算に148秒、その後の行列を分解したデータの書き出しに50秒かかったよ、という意味のようです。

え、これってめちゃくちゃ速くない?

さて、結果としてsvd_out.U、svd_out.S、svd_out.Vという3つのファイルが生成されました。

なお、octave等でsvdを使って次元縮退させる際は特異値分解の後に、S(固有値を降順にならべた配列)の累積寄与率(?)を自分で計算して、再度 U x sqrt(S) などすることで結果のマトリックスを得るという手順でしたが、redsvdはこの時点でそこまでやってくれています。

つまりsvd_out.Uがそのものズバリ、ということのようです。(たぶん)

なので、上のperlスクリプトで「とっておいた」ラベルのファイル(もとの検索クエリ文字列が順番に書かれたもの)とくっつければ、「200次元に圧縮された行列」のできあがりです。(たぶん)

検証してみる

上で「たぶん」と書いたのは、本当にこれで合ってるのか、結果の行列睨んでいてもわからないからです。

なんとかして検証しなければ、ということで簡単な「潜在的意味インデキシング風」なスクリプトを用意します。(以前も紹介したネタですが)

ここまでの処理で出来上がった「検索クエリーのSVD行列」をすべてメモリにのっけて、適当な文字列を入力として受け取ったら、メモリ上の全レコード分と総当たりに距離を測定して、近いものから10件を表示するスクリプトです。

総当たりなのですごく遅いですが、検証にはなるでしょう。

use strict; 
use warnings;
use Data::Dumper;

# データ復元
my $featured_vector;
open( VEC, "query_svd_matrix.txt" );
while (<VEC>) {
    chomp $_;
    my @f = split( "\t", $_ );
    my $key = shift @f;
    $featured_vector->{$key} = \@f;
}       
close(VEC);
    
# 標準入力からのデータ取得
print "INPUT NAME : ";
my $query = <>;
chomp $query;
my $query_vec = $featured_vector->{$query};
    
# 近傍検索
my @sorted
    = sort { $a->{dist} <=> $b->{dist} }
    map {
    my $name = $_; 
    my $vec  = $featured_vector->{$name};
    my $dist = distance( $query_vec, $vec );
    {   name => $name,
        dist => $dist
    }
    }
    keys %$featured_vector; 
        
# 上位10件を表示
for ( 1 .. 10 ) {
    print Dumper shift @sorted;
}   

# ユークリッド距離を計算する関数
sub distance {
    my $vector_1 = shift;
    my $vector_2 = shift;

    my @vec1 = @$vector_1;
    my @vec2 = @$vector_2;

    my $sum;
    for my $i ( 0 .. $#vec1 ) {
        my $d = ( $vec1[$i] - $vec2[$i] )**2;
        $sum += $d;
    }
    my $distance = sqrt($sum);
    return $distance;
}

さっそく走らせてみます。
まずはラーメン。

[miki@godzilla work]$ perl test.pl
INPUT NAME : ラーメン
$VAR1 = {
'name' => 'ラーメン',
'dist' => 0
};
$VAR1 = {
'name' => 'ぼぶ ラーメン紀行',
'dist' => '0.0177238305396999'
};
$VAR1 = {
'name' => 'とん太ラーメン',
'dist' => '0.0211681702090663'
};
$VAR1 = {
'name' => 'ラーメン 醤油亭',
'dist' => '0.0218072035346121'
};
$VAR1 = {
'name' => 'らーめん',
'dist' => '0.0235849467881528'
};
$VAR1 = {
'name' => '百福ラーメン',
'dist' => '0.0261412958171549'
};
$VAR1 = {
'name' => 'ラーメン虎ジ',
'dist' => '0.0265464345063513'
};
$VAR1 = {
'name' => 'ラーメン 二郎',
'dist' => '0.0292685444803803'
};
$VAR1 = {
'name' => 'ラーメン 彩味',
'dist' => '0.0299031733934711'
};
$VAR1 = {
'name' => 'げんこつ ラーメン',
'dist' => '0.0302795406669256'
};
[miki@godzilla work]$

おお!見事にラーメン関連のクエリが並びました。200次元に圧縮してもちゃんと意味が残ってたんですね!

おつぎは今話題のコレ。

INPUT NAME : 尖閣諸島
$VAR1 = {
'name' => '尖閣諸島',
'dist' => 0
};
$VAR1 = {
'name' => '日中尖閣諸島問題',
'dist' => '0.0146660394449217'
};
$VAR1 = {
'name' => '尖閣諸島領海',
'dist' => '0.0250298066312946'
};
$VAR1 = {
'name' => '尖閣諸島 中国人',
'dist' => '0.025504166345913'
};
$VAR1 = {
'name' => '中国 尖閣諸島',
'dist' => '0.030786449957733'
};
$VAR1 = {
'name' => '尖閣諸島 中国なんかに',
'dist' => '0.030786449957733'
};
$VAR1 = {
'name' => '尖閣諸島 中国のこうどうは?',
'dist' => '0.0325268407472967'
};
$VAR1 = {
'name' => '台湾 中国 尖閣',
'dist' => '0.0345627984399412'
};
$VAR1 = {
'name' => '尖閣諸島 アメリカ',
'dist' => '0.0360544180926554'
};
$VAR1 = {
'name' => '中国 尖閣',
'dist' => '0.0400427653890188'
};

ふむ。。。不穏な空気を感じますね。。。


最後にオマケでもう1発。

INPUT NAME : 焼肉
$VAR1 = {
'name' => '焼肉',
'dist' => 0
};
$VAR1 = {
'name' => '焼肉キング',
'dist' => '0.0354135623031629'
};
$VAR1 = {
'name' => '焼肉の仁',
'dist' => '0.0355411440586822'
};
$VAR1 = {
'name' => '焼肉店',
'dist' => '0.0363344784467866'
};
$VAR1 = {
'name' => 'ホルモン市場 じゅうじゅう',
'dist' => '0.0388911970759451'
};
$VAR1 = {
'name' => '焼肉幸 四日市',
'dist' => '0.0394660728474471'
};
$VAR1 = {
'name' => '焼肉 チェーン',
'dist' => '0.0395717680550162'
};
$VAR1 = {
'name' => '焼肉 チェーン 安',
'dist' => '0.0395717680550162'
};
$VAR1 = {
'name' => 'プルコギ',
'dist' => '0.0398892503564557'
};
$VAR1 = {
'name' => '焼肉明洞',
'dist' => '0.0399250527864397'
};

ホルモンとかプルコギとか、直接「焼肉」というコトバではないものがしっかり意味的に「近い」と評価してくれてるあたり、いいですね。

検証してみる その2

その2としてbayonでクラスタリングしたみたよ、って内容を書こうと思ったんですが、眠くなって来たので、省略。

ポイントというか、気になったことだけ書いておきます。

元のデータをbayonで 「-l 1.5」という条件でクラスタリングしたら25000くらいのクラスタになったんですが、redsvdで次元圧縮したデータをくわせたら13000程度のクラスタにしか分割されませんでした。

つまり、SVD(特異値分解)による潜在的意味インデキシングだと、単にサイズをちいさくするだけでなく、ノイズもカットしてることになるので、その影響で細かいクラスタには分かれなくなったんだと思います。

「ノイズをカットする = 大事なところだけにする = 特徴がよりはっきりする = あんまり細かくは分けられない」

といったところでしょうか。

当方シロウトにつき、識者の意見求ム。

まとめ

redsvdはえー。

岡野原さんすげー。



おわり。

Hadoopに入門してみた - セットアップからHadoop Streaming まで -

大規模データを処理する必要が出て来たので、Hadoopを導入してみることになりました。

以下、導入メモです。

セットアップ

以下のような構成で試してみます。環境はCentOSです。

マスター(host001)   ━┳  スレーブ(host002)
                       ┣ スレーブ(host003)
                       ┣ スレーブ(host004)
                       ┗ スレーブ(host005)

まずは各マシンにJavaをインストール。JDK1.6を落として来てrpmでインストールするか、yum install java-1.6.0*などとたたけばOKです。(rpmでインストールする場合は http://java.sun.com/javase/ja/6/download.html から jdk-6u18-linux-i586-rpm.binをダウンロードして、実行権限を与えてルートで実行すればインストールできます。)


続いてマスターノードにHadoop本体をダウンロードします。 ここら辺から適当に落としてきます。
tarballを落として来たら適当な場所で展開し、以下の設定ファイルをいじります。

conf/hadoop-env.sh

JAVA_HOMEは設定必須です。javayumでインストールした場合は export JAVA_HOME=/usr/lib/jvm/java-1.6.0-openjdk-1.6.0.0/ とか。rpmで入れた場合は export /usr/java/default にしておけばよいでしょう。

core-site.xml

マスターノードのホスト名を書き入れます。

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<!-- Put site-specific property overrides in this file. -->

<configuration>
  <property>
    <name>fs.default.name</name>
    <value>hdfs://host001:9000</value>
    <final>true</final>
  </property>
</configuration>
hdfs-site.xml

今回は試しなのでレプリケーションを1としました。

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<!-- Put site-specific property overrides in this file. -->

<configuration>
  <property>
    <name>dfs.replication</name>
    <value>1</value>
  </property>
</configuration>
mapred-site.xml

job trackerにはマスターノードのホストを指定します。mapperとreducerは各々3個に指定してみました。

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<!-- Put site-specific property overrides in this file. -->

<configuration>
  <property>
    <name>mapred.job.tracker</name>
    <value>host001:9001</value>
  </property>

  <property>
    <name>mapred.tasktracker.map.tasks.maximum</name>
    <value>3</value>
  </property>

  <property>
    <name>mapred.tasktracker.reduce.tasks.maximum</name>
    <value>3</value>
  </property>

</configuration>

以上の設定ファイルを書いたら、hadoopディレクトリごと全サーバにrsyncします。
Hadoop本体の設置は以上で終わり。意外と簡単ですね!


解析対象となるファイルを設置する

Hadoopで解析させたいファイルをhdfs上に設置します。

hdfsへのファイルのコピーはマスターノード上で bin/hadoop fs コマンドを使って行ないます。ここでは/data/logs/以下にapacheアクセスログが1ヶ月分置いてあると仮定しましょう。

その場合のコマンドはこんな感じになります。

[miki@host001 hadoop]$ bin/hadoop fs -put /data/logs/* input

上記のコマンドを叩くことで分散ファイルシステム上へのコピーが始まります。

デフォルト設定だと各スレーブの /tmp/hadoop-ユーザ名/dfs の下に64Mごとのチャンクに分割されたファイルが格納されていきます。

ためしにコンソールを複数立ち上げて、host002〜host005 で各々 watch -n 0 "du -s /tmp/hadoop-ユーザ名/dfs" などとすると、1台づつ順繰りにデータが渡されて来る様子を確認できます。

ここら辺、もっと一気に並列でどばっと撒いてくれると短時間で済むと思うんですが、どうやらそうなってはいないようです。Hadoopの全行程において、この「分散ファイルシステムへのコピー」は相当時間のかかる行為なので、イライラします。

なお後からわかったんですが、実は今回試した環境が100Mbpsしかスループットが出ないswitchに収容されていたため、600Gのデータを撒くのに、12時間もかかってしまいました。まずはギガビット出るようにしておかないとお話にならないようですね。

さてさて、なにはともあれhdfsへの転送が終了したので、確認してみます。

[miki@host001 hadoop]$ bin/hadoop fs -lsr

/user/ユーザ名/input/ というhdfs上のディレクトリにアクセスログが格納された様子がわかると思います。あとはこのhdfs上のデータに対してmap-reduceの処理を実行させていくことになります。

Hadoop Streaming を使ってmapperとreducerを書く

さて、ここまでくれば後は map-reduceの処理を走らせるだけなんですが、実は hadoop-streaming というAPIを使えば、javaでなくとも好きな言語でmapperとreducerを書くことができます。その際のルールは「標準入力からデータをもらって標準出力に書き出す」というきわめて単純なものとなっています。ありがたい!

というわけで、perlでmapperとreducer書いてみました。

まずはmap.pl。

#!/usr/local/bin/perl

use strict;
use warnings;

my $counter;

while(<STDIN>){
    chomp $_;
    my @f = split(" ", $_);
    $counter->{$f[5]}++;
}

while(my ($key, $value) = each %$counter){
    print $_,"\t", $counter->{$_},"\n";
}

標準入力からログを1行づつ読み込んで、適当にsplitなりなんなりして、データを数え上げます。

ここでは自前で$counterを設けていますが、どうせreducerに渡される前後でも同じようなことは処理されるので、これはなくてもいいです。メモリの無駄かも。その場合は単純に print $_,"\t1\n"; としておけばいいでしょう。

なお、きわめてシンプルなスクリプトを例示してしまいましたが、実際にはクロールやDB問い合わせ、エンコード/デコードなどなど、イロイロな処理をすることになるでしょう。

また現実的にはシンプルなkey => valueではなくて、多段のハッシュ構造などでデータを渡したいことが多いでしょう。その場合は key の部分に適当な文字列をセパレータにして複数のデータ項目を格納してしまえばよいと思います。reducerで分解すればよいので。


というわけで、おつぎはreduce.pl

#!/usr/local/bin/perl
use strict;
use warnings;

my $counter;
while(<STDIN>){
    chomp $_;
    my ($key ,$value) = split "\t";
    next if !($key && $value);
    $counter->{$key} += $value;
}

for( sort { $counter->{$a} <=> $counter->{$b} } keys %$counter ){
    print $_,"\t", $counter->{$_}, "\n";
}

見てわかる通り、reducerもシンプルです。もう好きなように書けばいいです。

ちなみに、naoyaさんが2年くらい前にhadoop streamingの記事を書いています。
その中でイテレータつかったより効率的なフレームワークを作られているので、本格的に導入する際にはこれを使うといいでしょう。


mapperとreducerを準備したので実際に処理を走らせてみます。

マスターノードで以下のコマンドを叩きます。

[miki@host001 hadoop]$ bin/hadoop jar contrib/streaming/hadoop-0.20.2-streaming.jar -input /user/miki/input -output output -mapper /home/miki/hadoop/perl_script/map.pl -reducer /home/miki/hadoop/perl_script/reduce.pl

hadoop-streamingを実行してくれるjarファイルはcontribの下にあるので、bin/hadoop jar で呼び出します。あとはhdfs上の -input と -outputの指定、それと -mapperと -reducerを指定します。

なお、mapperとreducerを絶対パスで直接指定していますが、そうする場合には事前に各スレーブ(host002からhost005まで)にperl_scriptをrsync しておく必要あります。またそんな面倒なことをしなくても、-file というオプションを指定することで任意のファイルを実行時にスレーブに転送することもできるようです。


map-reduceの処理が始まるとコンソール上に進捗状況が表示されます。

おわったらhdfs上の/user/ユーザ名/output/part-00000というファイルに結果が出力されるはずなので、 それをlinuxファイルシステムに持ってきます。

[miki@host001 hadoop]$ bin/hadoop/fs get /user/ユーザ名/output/part-00000 /home/ユーザ名/output.txt

これで結果ファイルを取得できました。お疲れ〜。

まとめ

Hadoopの導入は意外と簡単でした。今回試してみた環境には古いサーバ(RedhatEL4)も混ざっていたんですが、Javaの導入も問題なかったしHadoopもちゃんと動いてくれました。

「map-reduceのメリットは最低でも20台くらいないとわからない」なんて言いますが、まずは4〜5台程度で試してみることをオススメします。

というのは、map-reduceのカッコいい側面だけではなく、まずは小さい規模でシステムを組んでみて、どこらへんにどれくらい時間や負荷がかかるのか、総合的なコスト感というものを把握すべきかと思います。

その上で本当にイケルと確信したならば数十台規模のシステムを構築すべきかな、と感じました。

規模が小さくて簡単な処理だったら、実は普通に集計スクリプトを書いて複数プロセスで動かした方が速い場合もあるしね。(少なくともhdfsへの大量データコピーはかなり時間がかかります!)


とはいえ、Hadoop面白かったです。そのうちHiveを試してみます。