Tokyo Cabinet + Starmanで住所検索

高速なKVSであるTokyo Cabinetと高速なWebサーバStarmanを試してみたいなぁ、と思い、Ajaxを使った簡単な住所検索アプリを作ってみました。

※wataさんが、Tokyo Cabinetの後継のKyoto Cabinetを「Kyoto Cabinet + Starmanで住所検索 ...を試してみた - ワタブログ」で紹介されています。

PlackとStarmanをインストール

CPANでPlackとStarmanをインストールします。

% sudo cpan Plack
% sudo cpan Starman

Tokyo CabinetとPerl用APIをインストール

ソースパッケージとPerl用APIを「データベースマネージャ Tokyo Cabinet」からダウンロードします。

tokyocabinet-1.4.45.tar.gz
perlpkg/tokyocabinet-perl-1.33.tar.gz

まずはTokyo Cabinetのインストール。私の環境(Ubuntu 9.10)ではbzlib.hが不足してたので、先にlibbz2-devをインストールしておきました。

% tar xzvf tokyocabinet-1.4.45.tar.gz
% cd tokyocabinet-1.4.45
% ./configure --prefix=/usr/local/tokyocabinet
% make
% sudo make install

次にPerl用APIをインストールします。

% tar xzvf tokyocabinet-perl-1.33.tar.gz
% cd tokyocabinet-perl-1.33
% PATH=$PATH:/usr/local/tokyocabinet/bin perl Makefile.PL
% make
% make test
% sudo make install

KEN_ALL.tchを作成

住所検索の情報となる郵便番号データを日本郵便のサイト「郵便番号データダウンロード - 日本郵便」からダウンロードします。“読み仮名データの促音・拗音を小書きで表記しないもの”の“全国一括”のken_all.lzhです。

これを展開したKEN_ALL.CSVから、以下のスクリプトを使ってTokyo Cabinetのハッシュデーベースファイルを作ります。今回作る住所検索アプリでは郵便番号を1桁入力した時点から候補を表示するようにしたいので、1 桁~7桁のパターン毎に10個までの候補を記録しておきます。テーブルデータベースを使うともっとスマートに構成できそう?
また、Perl用APIはOOで扱う事ができますが、今回は手軽にtieを使いました。

#!/usr/bin/perl

use 5.008001;
use utf8;

use strict;
use warnings;

open my $ken_all_csv, '<:encoding(shiftjis)', 'KEN_ALL.CSV'
    or die 'failed to open KEN_ALL.csv';

my %ken_all_tmp;

{
    local $/ = "\x0D\x0A";

    while ( my $record = <$ken_all_csv> ) {
        my ($zip_code, @address) = ( split /,/, $record )[2, 6, 7, 8];

        for my $value ($zip_code, @address) {
            $value =~ s/\A "(.*)" \z/$1/xms;
        }

        if ( $address[2] eq '以下に掲載がない場合'
             or $address[2] =~ / の次に番地がくる場合 \z/xms
        ) {
            $address[2] = q{};
        }
        else {
            $address[2] =~ s/ 一円 \z//xms;
        }

        my $address = join q{}, @address;

        DIGIT:
        for my $digit ( 1 .. 7 ) {
            my $zip_code = substr $zip_code, 0, $digit;

            next DIGIT if @{ $ken_all_tmp{$zip_code} ||= [] } >= 10;

            push @{ $ken_all_tmp{$zip_code} }, $address;
        }
    }
}

use TokyoCabinet;

use constant OWRITER => TokyoCabinet::HDB::OWRITER;
use constant OCREAT  => TokyoCabinet::HDB::OCREAT;

tie(my %ken_all, 'TokyoCabinet::HDB', 'KEN_ALL.tch', OWRITER | OCREAT)
    or die 'tie error';

while ( my ($zip_code, $addresses) = each %ken_all_tmp ) {
    $ken_all{$zip_code} = join q{<br />}, @$addresses;
}

PSGIを作成

住所検索アプリとなるプログラムをPSGI仕様で作成します。こちらもtieを使いました。
Ajaxリクエストのトリガーとする郵便番号の入力捕捉は、keydownイベントをそのまま使い、キーコードも使わずsetTimeoutでタイミングをずらして値をそのまま取得しています。

use strict;
use warnings;

use TokyoCabinet;

use constant OREADER => TokyoCabinet::HDB::OREADER;

my $html = do { local $/; <DATA> };

my $app = sub {
    my ($env) = @_;

    return [ 200, [ 'Content-Type' => 'text/html' ], [ $html ] ]
        if $env->{REQUEST_URI} !~ m{\A /search\?zip_code=(.*) }xms;

    my $zip_code = $1;

    tie(my %ken_all, 'TokyoCabinet::HDB', 'KEN_ALL.tch', OREADER)
        or die 'tie error';

    my $address = $ken_all{$zip_code} || q{};

    return [ 200, [ 'Content-Type' => 'text/html' ], [ $address ] ];
};

__DATA__
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>ken_all</title>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript"><!--
  google.load('jquery', '1.4'); // -->
</script>
<script type="text/javascript"><!--
  jQuery( function ($) {
      $('input#zip_code').keydown( function () {
          var $this = $(this);

          setTimeout( function () {
              $.get( '/search', { zip_code: $this.val() }, function (data) {
                  $('div#address').html(data);
              } );
          }, 0 );
      } );

      $('input#zip_code').focus();
  } ); // -->
</script>
</head>
<body>
<form action="/search" method="get">
  <div><input id="zip_code" type="text" /></div>
  <div id="address"></div>
</form>
</body>
</html>

PSGIを起動してブラウザでアクセス

作成したPSGIをStarmanで起動します。デフォルトのポート5000でWebサーバとして動作します。これで準備完了です。

% starman ken_all.psgi

ブラウザからWebサーバにアクセスします。ローカルホストであればURLはhttp://localhost:5000/です。テキスト入力欄に数値を入力すると、その下に瞬時に住所が表示されます。速い!