DokuWikiの日本語対応
DokuWikiのインストールマニュアル(日本語版)にしたがって普通にインストール。インストール画面で日本語を選択すれば、自動的に設定ファイルのconf/local.php
の中に
$conf['lang'] = 'ja';
と記述されて、ユーザーインターフェースの文字列などは日本語になる。 機能面での日本語対応を強化するために、次の改造を加える。
メールのエンコード
DokuWikiでは、文書を誰かが編集したときなどにお知らせのメールを送る機能があるが、そのメールのエンコードを自前のUTF-8化ルーチンで行っている。日本語のメールはUTF-8ではなくISO-2022-JPでエンコードするのが通例なので、内蔵のmb_send_mail関数が使える場合はそちらを使って$conf['lang']で指定したエンコードで送信するように変更する。
inc/mail.phpというファイルのfunction mail_sendという関数を変更。
function mail_send($to, $subject, $body, $from='', $cc='', $bcc='', $headers=null, $params=null){ if(defined('MAILHEADER_ASCIIONLY')){ $subject = utf8_deaccent($subject); $subject = utf8_strip($subject); }
の直後に
global $conf; if (function_exists('mb_send_mail')) { $header = "From: $from\nCc: $cc\nBcc: $bcc"; $header .= $headers; $header = trim($header); mb_language($conf['lang']); if($params == null){ return @mb_send_mail($to,$subject,$body,$header); }else{ return @mb_send_mail($to,$subject,$body,$header,$params); } }
を追加。
検索の日本語対応
DokuWikiでは自前の全文検索エンジンを搭載している。
新しいページを登録するときに、全文検索のインデックス(なんという単語がどのファイルにいくつ入っているかを記録した索引)ファイルの中に、登録したページの索引情報を記録するようになっているのだが、ページの中から単語を切り出すのに空白文字で区切られたところを単語と認識して処理している。
日本語のように、単語の間に空白文字を書かない文書をこのプログラムに通すと、日本語の文字が全部ひとつながりになって1単語と認識されて登録されてしまう。たとえば、“DokuWikiでは自前の全文検索エンジンを搭載している。”という文は“DokuWiki”と“では自前の全文検索エンジンを搭載している。”という2つの単語として索引に登録されてしまう。このため、例えば“全文検索”とか“エンジン”とかで検索してもこのページはヒットせず、“では自前の全文検索エンジンを搭載している。”で検索して初めてヒットする、ということになってしまう。
これを解決するために、索引に登録するところで日本語の文章の単語の間に空白を入れる処理を追加する。日本語の文章を分かち書きに変換するプログラムとしてはkakasiが有名だが、kakasiは標準ではUTF-8のテキストに対応していない。
nkf -wE | kakasi -wc | nkf -wE
などと、前後に文字コード変換プログラムのnkfやiconvを使って無理やりUTF-8→EUC-JP→UTF-8 という変換をするという手もあるが、効率が悪いし、なによりかっこ悪い。そこで、UTF-8に対応していて、kakasiより数倍早いという触れ込みのMeCabを使ってみる。
MeCabのインストール
MeCabの説明ページの説明に従って必要なファイルをダウンロードしてインストールする。 別にダウンロードする辞書ファイルは「ipadic-2.4.4/2.5.0/2.5.1のいずれか」と説明されているが、どうやら2.6.1までは使えるようだ。2.6.2以降は辞書の構造が変わっているために使えない。
UTF-8の環境で使えるようにconfigureする。
$ ./configure --prefix=/usr/local/nls --disable-shared --with-charset=utf8
/usr/local/の下に自由にファイルやディレクトリを作れる場合は--prefix=/usr/local/nls
は不要。私が使っているWestHostのサーバーでは管理者アカウントは本物のroot権限ではなく/usr/local/man
など一部のディレクトリの下にアクセスできないため、別のディレクトリを指定している。ここではnls
というディレクトリを作ってそこに日本語対応用のプログラムやライブラリをまとめて入れているが、名前はなんでもよい。たとえば、/usr/mylocal/の下にlibやincludeなどを作り、/etc/ld.so.confに/usr/mylocal/lib/を追加するなどというのもあり。
--disable-shared
は、シェアードライブラリを使わない指定。このほうがパフォーマンスが少しよくなるはず。
--with-charset=utf8
を指定しないとEUC-JPでビルドされる。
configureしたら、あとはmakeするだけ。
$ make $ make install
インストールしたら、正しく分かち書きできるかどうか試してみる。
$ /usr/local/nls/bin/mecab -O wakati 上野発の夜行列車降りたときから青森駅は雪の中 上野 発 の 夜行 列車 降り た とき から 青森 駅 は 雪 の 中 ^D
のように、入力した文字が分かち書きされて表示されればOK。
全文検索インデクサーの改造
MeCabで分かち書きをさせるには、mecab -O wakati
。標準入力に元のテキストを入力すると標準出力に分かち書きしたテキストが出てくる。
PHP 4.3で追加されたproc_open()関数を使って、パイプ渡しでテキストを分かち書き処理させる。
inc/indexer.php
どこか先頭に近いところに
define ('PRE_TOKENIZER', '/usr/local/nls/bin/mecab -O wakati');
を追加。これで、分かち書きをする外部プログラムの名前(シェルで呼び出すときのコマンド)を定数定義。kakasiを使うなら'nkf -We | kakasi -wc | nkf -Ew'
。
本当は$conf['pre_tokenizer']
か何か、globalな設定用変数に入れるようにしたほうがお行儀がいいけど、とりあえずquick hack。
あとは、検索対象の文字列をtoken(単語)に切り分ける部分の直前で、この外部プログラムを呼び出すようにするだけ。 function idx_getPageWords($page)の
list($page,$body) = $data;
と
$body = strtr($body, "\r\n\t", ' ');
の間に
if(function_exists(proc_open) && defined('PRE_TOKENIZER')) { $dspec = array( 0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("file", "/dev/null", "w") ); $process = proc_open(PRE_TOKENIZER, $dspec, $pipes); if(is_resource($process)) { stream_set_blocking($pipes[0], FALSE); stream_set_blocking($pipes[1], FALSE); fwrite($pipes[0], $body . "\n"); fclose($pipes[0]); $body = ''; while(!feof($pipes[1])) { $body .= fgets($pipes[1], 32768); } fclose($pipes[1]); proc_close($process); } }
を追加
function idx_tokenizer($string,&$stopwords) の先頭
$words = array();
の後ろに
if(function_exists(proc_open) && defined('PRE_TOKENIZER')) { $dspec = array( 0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("file", "/dev/null", "w") ); $process = proc_open(PRE_TOKENIZER, $dspec, $pipes); if(is_resource($process)) { stream_set_blocking($pipes[0], FALSE); stream_set_blocking($pipes[1], FALSE); fwrite($pipes[0], $string . "\n"); fclose($pipes[0]); $string = ''; while(!feof($pipes[1])) { $string .= fgets($pipes[1], 32768); } fclose($pipes[1]); proc_close($process); } }
を追加。
DokuWikiの最近のバージョンでは、日本語を含むアジア圏の文字を1文字1単語とみなして検索する修正が入っているが、これを使うと例えば「文字を探す」で検索すると「文」「字」「を」「探」「す」のすべての文字が検索結果でハイライトされたりして具合がよくない。上記の修正を加えたときは、idx_tokenizer($string,&$stopwords)関数にある
$asia = @preg_replace('/('.IDX_ASIAN.')/u','\1 ',$word); if(!is_null($asia)) $word = $asia; //recover from regexp failure
という2行をコメントアウトする。 また、indexer.phpにwordlen()という関数があるが、これも同じくアジア圏の文字は1文字1単語とみなす処理が入っているので
function wordlen($w){ // $l = strlen($w); $l = utf8_strlen($w); //// If left alone, all chinese "words" will get put into w3.idx //// So the "length" of a "word" is faked //if(preg_match('/'.IDX_ASIAN2.'/u',$w)) // $l += ord($w) - 0xE1; // Lead bytes from 0xE2-0xEF return $l; }
と変更。
それから、同じくindexer.php の idx_getIndexWordsSorted() 関数の中に、
if ($wlen < 3 && $wild == 0 && !is_numeric($xword)) continue;
という部分があるが、このままだと3文字より短い単語を検索できない。英語などではそれでもよいのだが、日本語の場合1~2文字の単語も検索できないと困るので、これを
if (preg_match('/[^0-9A-Za-z]/u', $string) && $wlen < 3 && $wild == 0 && !is_numeric($xword)) continue;
と書き換える。
2005-12-8 - Mecabのプロセスがハングアップして残ってしまうのを避けるためにstream_set_blocking()
を追加
2007-7-29 - DokuWikiの最近のバージョンにあわせて、修正箇所の説明を変更
全文検索インデクサーの不具合修正
以上で全文検索の日本語対応改造は完了だが、本日現在の最新バージョン(20050922)には、全文検索機能にもとからバグがあるようで、ASCII文字以外の文字が混ざった単語を検索すると、検索結果ページで表示されるヒット数が全部1になってしまう。
これに対する修正を加える。
上と同じinc/indexer.php
ファイル、idx_getPageWords
関数の中の
$words[$w] = $c + (isset($words[$w]) ? $words[$w] : 0);
という行を
$words[$w] = $c * $count + (isset($words[$w]) ? $words[$w] : 0);
に変更(‘* $count
’という部分を追加)。
この変更後、修正したり新規に追加したりして保存しなおしたファイルは、検索結果のヒット数が正常に表示されるようになる。 http://bugs.splitbrain.org/?do=details&id=653 にバグ報告済み。
追記: この不具合は、現在配布されているバージョンでは修正されています。