wata.dev

全文検索 Groonga を使って perl で Google Suggest ならぬ Groonga Suggest を作る

前回もちょっと書いた全文検索 Groonga がすごい便利なので、そのことが少しでも伝わるように、
例によって日本郵便が提供してる郵便番号データを使って、Google Suggest 風の何かを作ってみました(伝わらないとか言わないで!)。あと Yappo さんがやられている優良バインディング案件が待ち遠しいです。

今回使うものは全部 https://github.com/wata/Groonga-Suggest に。

7/13 追記: データのロードの部分(ken_all.pl)を書き直してもらったので、試すときは github にあるものを見てください。

1. DBの作成

作成するテーブル、カラムはこんな感じ。

table_create --name KEN_ALL --flags TABLE_HASH_KEY --key_type UInt32
table_create --name IndexKEN_ALL --flags TABLE_PAT_KEY|KEY_NORMALIZE --key_type ShortText --default_tokenizer TokenBigram

column_create --table KEN_ALL --name address --flags COLUMN_SCALAR --type ShortText
column_create --table KEN_ALL --name yomi --flags COLUMN_SCALAR --type ShortText
column_create --table IndexKEN_ALL --name index_ken_all --flags COLUMN_INDEX|WITH_POSITION|WITH_SECTION --type KEN_ALL --source _key,address,yomi

IndexKEN_ALL という名前のテーブルが全文検索するために必要な語彙表です。その他オプションなどについてはドキュメントをチェックしてみてください。これを適当に ken_all.grn とかで保存したら /home/wata/db/groonga に移動、新しく Groonga-Suggest というディレクトリを切って、groonga_suggest.db に ken_all.grn を流しこみます。

$ mkdir Groonga-Suggest
$ cd Groonga-Suggest
$ groonga -n groonga_suggest.db < /home/wata/work/Groonga-Suggest/ken_all.grn

エラーみたいのが出なければOK。

2. データのロード

次にダウンロードしてきた郵便番号データを groonga に読み込ませます。そのために、

  • CSV から JSON へ変換する
  • その際半角カナをひらがなに、など細かな修正を加える
  • JSON 形式にしたデータを groonga コマンドでロードする

などの作業が必要です。それを全部済ませるスクリプトがこれ。

ken_all.pl

use strict;
use utf8;
use Lingua::JA::Moji qw/hw2katakana kata2hira/;
use JSON;
use IPC::Cmd qw/run/;

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

#my $data;
while ( my $record = <$ken_all_csv> ) {
    my ($zip_code, @tmp) = ( split /,/, $record )[2 .. 8];

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

    my @yomi = @tmp[0 .. 2];
    if ( $yomi[2] eq 'イカニケイサイガナイバアイ'
         or $yomi[2] =~ / ノツギニバンチガクルバアイ \z/xms
       ) {
        $yomi[2] = q{};
    }
    else {
        $yomi[2] =~ s/ イチエン \z//xms;
    }
    my $yomi = join q{}, @yomi;
    $yomi = hw2katakana($yomi);
    $yomi = kata2hira($yomi);

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

    my $data = { _key => $zip_code, yomi => $yomi, address => $address };
    my ($ok, $err) = load->($data);
    die $err unless $ok;
#    push @{ $data }, { _key => $zip_code, address => $address };
}

#my ($ok, $err) = load->($data);
#warn $ok ? "complete" : $err;

sub load {
    my $data = shift;

    $data = encode_json($data);

    return run(
        command => [
            'groonga',
            '/home/wata/db/groonga/Groonga-Suggest/groonga_suggest.db',
            'load',
            '--values',
            "\'$data\'",
            '--table',
            'KEN_ALL',
        ],
        buffer  => \my $buf,
    );
}

(ほんとは1レコードずつロードさせるんじゃなくて、最後に一括してロードさせたかったんだけど、それだとうまくいかない。。。ので全部終わるまで30分ぐらいかかります。コメントアウトしてある部分はその軌跡。)

7/10 追記: groonga(単体サーバ)で大量データロード - WebService::Blog->new( user => ’hide_o_55’ ) では、データを書き出した後、標準入力という方法が紹介されていました。

3. groonga デーモンの起動

今回は groonga を http 経由で利用したいので、HTTP モードでデーモンを起動します。下のコマンドでプロトコルの指定(http)、DB の指定をしてます。ポート番号の指定をしてないので http://localhost:10041(デフォルト)で起動します。

$ groonga -d --protocol http /home/wata/db/groonga/Groonga-Suggest/groonga_suggest.db

ちなみにブラウザから http://localhost:ポート番号 にアクセスすると HTML + jQuery で構成された管理画面が見れます。ここからもテーブル作成したり、カラム追加したり、データ追加したり出来ます。

4. アプリ作成

js からリクエストがあったら groonga に問い合わせて結果を返す psgi がこれ。

app.psgi

use strict;
use Plack::Request;
use URI;
use Furl::HTTP;

my $app = sub {
    my $env = shift;
    my $req = Plack::Request->new($env);

    my $html = do { local $/;  };

    if ( my $q = $req->param('q') ) {
        my $ua = Furl::HTTP->new;
        my $u  = URI->new('http://localhost:10041/d/select');
        $u->query_form(
            table          => 'KEN_ALL',
            match_columns  => '_key,address,yomi',
            query          => $q,
            output_columns => '_key,address',
            sortby         => '_id',
            limit          => '100',
        );
        my ( undef, $code, undef, undef, $body ) = $ua->get($u);
        return [ 200, [ 'Content-Type' => 'application/json' ], [ $body ] ] if $code eq 200;
    }
    return [ 200, [ 'Content-Type' => 'text/html' ], [ $html ] ];
};

__DATA__
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Groonga Suggest</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js"></script>
<script type="text/javascript">
  $(document).ready(function() {
    $('input#search').keydown(function() {
      $.get('/search', {"q":$(this).val()}, function(json) {
        var data = json[1][0].slice(2).join('<br />');
        $('div#address').html(data);
      });
    });

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

えいっ!

$ plackup -r app.psgi

groonga-suggest

※出来上がったものについては雰囲気重視です

ブログの記事一覧