Unicode msearch 導入メモ

私のメインサイトでは namazu 2 でサイト内全文検索ができるようになっている。namazu 2 は UNIX 環境日本語全文検索エンジンの定番になって久しいが,基本的に ISO-2022-JP, Shift_JIS, EUC-JP の日本語文字コードを前提としており,多国語文書には適さない。ロシア語に限って言えば,Unicode ロシア語文書も検索できるのだけど(JIS の智慧のおかげ)。

最近ではサイト内検索エンジンをブログ管理システムに任せるか,Google のドメイン指定の検索窓を設けることで,自らインデックス管理自体をしないですむようになっており,独自に検索エンジンを設置する必要性は薄くなって来ている。ちなみに,Google のドメイン指定で自分のサイト内だけを検索するための検索窓は,以下のようなコードを自サイトのページに埋め込めばよい。

<form method="get" action="http://www.google.com/search">
  <input type="text" name="q" size="30" maxlength="255" value="" />
  <input type="hidden" value="検索対象ドメイン" name="as_sitesearch" />
  <input type="hidden" name="hl" value="ja" />
  <input type="hidden" name="ie" value="UTF-8" />
  <input type="submit" value="Google 検索" />
</form>
このサイト内なら—

私は受信したメールを MHonArc でアーカイブし,パスワードで文書セキュリティを保護することで,Web でどこからでも自分のメールを見られるようにしている。このような用途では,Google を使うわけには行かず(非公開なんだから当然),どうしても独自に検索エンジンを設置しなければならない。

Unicode msearch

Katsushi Matsuda,毛流麦花両氏による msearch は Unicode 文書からインデキシングが可能である。よって多国語文書も本来の文字列で検索が可能である。そこで私も Unicode 版 msearch Ver. 1.52 を導入してみた。これで柔軟な多国語検索とメールアーカイブ検索が両立できるというわけだ。http://yasuda.homeip.net/msearch.html から私のサイト内の検索ができるようにしてみた(メール検索は当然ながら非公開)。検索結果画面は msearch のお仕着せをそのまま使っている。まだ画面右上の namazu 検索は生きている。そのうち整理するつもりである。

msearch は,茶筌で日本語検索対象語を抽出してインデキシングを行う namazu とは異なり,おそらく n-gram で全文インデックスを抽出している。これは検索モレを防ぎかつ高速化を実現するのに有効な方法である。ただし,msearch は多国語文字を取り扱うことができるとはいえ,いわゆる語形変化に追随できるようなインデキシング解析をしているわけではなさそうである。Google なら,нести (「携える」という意味のロシア語不規則変化動詞不定形) を入力すると,несут (三人称複数現在) などの変化形をも検索できる。さすがである。

msearch は PDF, Microsoft Word, Microsoft Excel, Microsoft PowerPoint をインデキシングするためのフィルタを持たないため,そのままでは原則 HTML 文書だけを扱うことになる。私自身は .doc, .xls, .ppt ファイルをプライベートで扱うことはないので,まったく困らないのだが,PDF だけはなんとしても検索できるようにしたい。以下,PDF も取り扱うことができるように msearch 環境を調整するメモをしるしておく。環境は Mac OS X Snow Leopard (インデックス作成) 及び FreeBSD 8.1-RELEASE (公開 Web 環境) である。掲載内容は無保証である。

Unicode 版 msearch インストール

Unicode msearch のダウンロード,インストールは『サイト内全文検索エンジン ― Unicode版msearch』に毛流麦花氏による懇切丁寧な解説があるので,そちらを参照する。アーカイブを解凍し,cgi-bin/msearch にリソースを格納するだけである。Perl 5 が必要である。/~user/ などの Web ユーザディレクトリ下で msearch を運用する場合は,public_html/cgi-bin/msearch のパーミッションが 644 あるいは 755 (他ユーザの書込権限なし) になっていないと,Premature end of script headers のエラーが出て動作しないので注意。

私の場合,PDF 処理その他でサイトのドキュメント・ツリーを書き換えてしまうオペレーションが発生するため,Apache22 の公開エリアとは別に ~/var/webindex ディレクトリ (以下「ワークツリー」) を作成し,そのなかにサイト・ドキュメント・ツリーをコピーするとともに,msearch リソース・ディレクトリをも設置した。ここで検索インデックスをローカル作成(msearch では,Web ブラウザからインデックス作成操作が可能であるが,これを使わず,サーバ・ローカルの端末でコマンド操作によりインデックス作成を行うことを「ローカル作成」と呼んでいる)し,できたものを Apache22 の公開エリア /usr/local/www/apache22/cgi-bin/msearch にコピーするという運用である。

msearch のサイト用独自設定は default.cfg で行う。本稿の試行では,とりあえず set $home= の右辺を私のサイト URL に書き換えただけである。

PDF インデキシング

msearch で PDF ファイルのインデックスを作成するには,いくつか注意事項があり,少し工夫が必要である。そのままで PDF をインデックス作成対象に指定すると,インデックス・ローカル作成プログラム genindex.pl (本稿で「インデクサ」とあるのはこれのこと) は異常終了する。

PDF はテキスト変換した上でインデクサに掛けるのが基本である。UNIX X11 PDF ビュア XPDF のユーティリティ pdftotext で PDF — テキスト変換を行った上でインデキシング実行すればよい。このときテキスト化されたファイルを元の PDF ファイルと同じ名称にしておかないと,検索結果のリンクで当該ファイルを参照できないので,テキスト変換結果で元 PDF ファイルを上書きしておく必要がある。PDF 以外でも wvWare (Microsoft Word 用),xlHtml (Microsoft Excel 用),pptHtml (Microsoft PowerPoint 用) の各 UNIX ソフトウェアを利用し HTML ないしテキスト形式に変換することで,msearch インデキシングが可能である。

第二の注意点として,HTML / XML 以外のファイルに対して msearch は BOM で Unicode エンコードを判断していることがある。BOM がないと,Web 検索結果画面上の当該ヒットエントリの文字が化けてしまうのである。普通,UTF-8 でテキストファイルを作成するとき,BOM を付けたりしないので,インデクサに掛ける前に UTF-8 BOM (十六進コード "EFBBBF") をワークツリーのテキスト変換後ファイルの先頭に書き込んでおく。これは echo, cat コマンドなどで簡単にできるのだけれども,私は,PDF に混在した不要な制御コード文字を取り除く目的と合わせて,これを行う簡単なプログラム chkucntlchr を書いた。PDF 以外のテキストファイルも同様の処置が必要である。

以上の処理を自動で行うシェルスクリプトのコード例を以下に示す。これを含んで,msearch インデックス作成の全体シェルスクリプト例を最後に掲げてある。

# PDF format conversion
$WRK=ワークツリー
$UCK=chkucntlchr # 制御コード削除・BOM 付加ツール
$STP=タイムスタンプファイル (前回実行時の日付属性をもつ空ファイル)
echo "*  Convert PDF to TEXT by pdftotext (XPDF)."
cd $WRK
for i in `find -L . -newer $STP -name "*.pdf"`
do
    pdftotext -enc UTF-8 -nopgbrk $i $i.txt
    if [ $? -eq 0 ]; then
        echo "**  pdftotext $i OK."
        $UCK < $i.txt > $i
        if [ $? -eq 2 ]; then
            echo "**  $i is empty. Ignore."
            rm -f $i
        fi
    else
        echo "**  pdftotext $i NG. Ignore."
        rm -f $i
    fi
    rm -f $i.txt
done

また,chkucntlchr ツールの Perl コードは以下の通りである。

#!/usr/bin/perl -w
# -*- coding: utf-8; mode: cperl; -*-
# chkucntlchr
# 2011(c) isao yasuda.
# - delete words including control characters (U+0001--U+0020, U+007F--U+00A0)
# - Add UTF-8 BOM (x'efbbbf') for msearch indexer
# - Return code 0: normal; 1: suppressed; 2: imput empty;
use strict;
use utf8;
binmode STDOUT, ":utf8";
my $flg = 0;
my $lc = 0;
my $utf8_bom = "\xEF\xBB\xBF";      # BOM for UTF-8
utf8::decode($utf8_bom);
print $utf8_bom;
while (<STDIN>) {
    chomp($_); $lc++;
    utf8::decode($_);
    my @line = split(/\s/, $_);
    foreach my $wd (@line) {
        if ($wd =~ /[\x{0001}-\x{0020}\x{007F}-\x{00A0}]/) {
            $flg = 1;
        } else {
            print "$wd ";
        }
    }
    print "\n";
}
if (! $lc) {
    print STDERR "*** $0: input empty.\n";
    exit 2;
}
if ($flg) {
    print STDERR "*** $0: suppressed control characters.\n";
} else {
    print STDERR "*** $0: no problem.\n";
}
exit $flg;


segmentation fault 対策

msearch インデクサに PDF を食わせるに際して,もっとも悩んだのはテキスト化した PDF でも,インデクサが segmentation fault エラーで異常終了する場合があることであった。インデクサからコールされる indexing.pl に罠を仕掛けて調査したところ,問題が二つ判明した。

まず第一には,サイトのインデックス対象ファイル数が多いと,UNIX の ulimit のファイルオープン数の制限に引っ掛かり,異常終了してしまう。ulimit -a で制限値を確認し,インデックス対象ファイル数よりも open files の設定値が小さければ,ulimit -n 数値 で値をファイル数よりも大きな値に設定する。

第二には,msearch インデクサは正規表現 s 演算子によって HTML タグの除去処理を行っているが,これをすべてのファイルに適用しており,テキスト変換された PDF ファイル中の文字列如何によっては誤動作してしまう。私のサイトの PDF は,LaTeX 多言語文書を dvipdfmx で処理した生成物が多い。LaTeX フォント・パッケージの enc ファイル(エンコーディング定義ファイル)によってはヘンな文字の羅列になることがあり,これでタグ判定の正規表現がぶっとんでしまったらしい。indexing.pl 791 行目を,HTML / XML でないときは実行しないように,以下の改変を行うと,アボートしないようになった。

#    $contents =~ s/<(?:[^"'>]|"[^"]*"|'[^']*')*>/ /g;  # オリジナル
    $contents =~ s/<(?:[^"'>]|"[^"]*"|'[^']*')*>/ /g if ($html_xml); # 対策

XPDF 多国語化

XPDF pdftotext の多国語対応について簡単にしるしておく。FreeBSD Ports,Mac OS X MacPorts では XPDF Japanese port が用意されており,これをインストールすれば日本語 PDF については扱うことが出来るようになる。しかし,多国語 PDF はダメ。

XPDF のサイトでは中国語,ギリシア語などいくつかの言語設定追加リソース・パッケージが公開されている。これらを用いて多国語対応設定ファイルを作成する。以下のシェルスクリプトを実行すれば,パッケージをダウンロード・展開した上で,カレントディレクトリに xpdfrc を生成する。これを $HOME/.xpdfrc として格納する。次に,パッケージのリソース xpdf-japanese 等を XPDF 管理ディレクトリ (FreeBSD なら /usr/local/share/xpdf) にコピーする。これで,pdftotext により,日本語のほか簡体中文,繁體中文,韓国語,ロシア語,ギリシア語,アラビア語,ヘブライ語などの Unicode テキストが出力できるようになる。ただし,この実行で生成された xpdfrc には PDF 表示の際に必要となるフォント定義がないので,あくまで pdftotext テキスト変換用途と理解いただきたい。表示も含めた設定は XPDF ドキュメントを参照して displayNamedCIDFontTT 定義に好みの TrueType, OpenType フォントを指定するなりして調整してほしい(私は UNIX 環境での PDF 表示は Adobe Reader を使っているのでこのへんは放置している)。

#!/bin/sh
# Download XPDF language packs
WGET="wget -nH -nd "
XPDFSITE="ftp://ftp.foolabs.com/pub/xpdf"
# XPDFETC は xpdfrc があるディレクトリに変更する
XPDFETC="/usr/local/etc"
$WGET $XPDFSITE/xpdf-arabic.tar.gz
$WGET $XPDFSITE/xpdf-chinese-simplified.tar.gz
$WGET $XPDFSITE/xpdf-chinese-traditional.tar.gz
$WGET $XPDFSITE/xpdf-cyrillic.tar.gz
$WGET $XPDFSITE/xpdf-greek.tar.gz
$WGET $XPDFSITE/xpdf-hebrew.tar.gz
$WGET $XPDFSITE/xpdf-japanese.tar.gz
$WGET $XPDFSITE/xpdf-korean.tar.gz
$WGET $XPDFSITE/xpdf-latin2.tar.gz
$WGET $XPDFSITE/xpdf-thai.tar.gz
$WGET $XPDFSITE/xpdf-turkish.tar.gz
# Expand archives
for i in *.tar.gz; do tar zxvf $i; done
# make xpdfrc
cp $XPDFETC/xpdfrc .
for i in `find . -name "add-to-xpdfrc"`
do cat $i >> xpdfrc; done

インデキシング用シェルスクリプト

最後に私が自サイト用に作成したインデキシング用シェルスクリプトを掲載しておく。Mac OS X Snow Leopard 環境である。HTML ソース Subversion 管理エリアからワークツリーに更新ファイルをコピーし,ワークツリーでインデキシングしたのち,Mac 上の Apache22 cgi-bin 試験環境にインデックスファイル default.idx をコピーする。これでできた Mac 上の default.idx をそのまま FreeBSD サーバの msearch 環境に転送して公開するという運用形態である。

私は自サイトの HTML 等公開コンテンツを Subversion でバージョン管理しており,ソースを commit すると Subversion の commit スクリプトが動作して,自動的に更新コンテンツを Apache22 のドキュメント・ツリーにコピーするようにしている。このスクリプトを変更し,commit のタイミングで msearch インデクス作成スクリプトを呼び出して,検索インデクスを自動的に更新することもできる。crontab に登録して定時自動実行するのもよいと思う。

上記の PDF テキスト変換以外にも,HTML の iso-2022-jp to UTF-8 変換なども実行内容に含まれている。シェル内の各パス設定,サイト設定は私の環境そのままなので,もしこれを活用する方がいらっしゃるのなら,自分の環境に応じて書き換えないといけない。もちろん,中味をよく確認し,私のいい加減なコードにヘンなところがあれば手直しいただいたほうがよい。

#!/bin/sh
# -*- coding: utf-8; -*-
# msearch index generator for ISOLDE
# - coded by isao yasuda, 1 Apr. 2011
#
# DESCRIPTION
# -----------
# 1.前回タイムスタンンプより新しいページをSRCからWRKに格納する。
# 2.JISコードのページをUTF-8に変換して格納する。(nkf, sed)
# 3.PDFをテキスト変換してWRKに格納する。
#     rc OK: .pdf の内容はテキストファイル 
#     rc NG: .pdf, .pdf.txt を削除する
# 5.テキスト変換 OK のものの制御コードを削除する。(chkucntlchr)
# 4.WRKでインデックスを生成する。(genindex.pl
# 5.インデックスをWRKからPUBに格納する。
#
# CAUTION
# -------
# 1.初期作成時はファイル数が多いのでWRKにsite treeをコピーしておく。
#
 
# SRC: ページソースエリア
# WRK: ワークエリア
# PUB: 公開エリア (ここでは触らない)
WWW=/usr/local/www/apache22
SRC=/home/isao/src/noxinsomniae
PUB=$WWW/data
CGI=$WWW/cgi-bin/msearch
WRK=/home/isao/var/webindex/website
MSE=/home/isao/var/webindex/msearch
STP=/home/isao/var/webindex/stamp
TMP=/home/isao/var/webindex/tmp
UCK=$MSE/chkucntlchr
NKF=/usr/local/bin/nkf
PDFTOTEXT=/usr/local/bin/pdftotext
 
echo "********************************************************"
echo "*  msearch Index Generation Start `date '+%Y/%m/%d %H:%M:%S.'`  *"
echo "********************************************************"
# Copy SRC to WRK
echo "*  New Files Archiving."
cd $SRC
find -L . -newer $STP -type f | grep -v '.svn' |\
xargs tar cf - | ( cd $WRK; tar xvf - )
cd $WRK
 
# Convert iso-2022-jp to utf-8
echo "*  Convert ISO-2022-JP pages to UTF-8 pages."
cd $SRC
for i in `find -L . -newer $STP -name "*.html"`
do
    $NKF -w $i |\
    sed -e 's|charset=[Ii][Ss][Oo]-2022-[Jj][Pp]|charset=UTF-8|g' > $WRK/$i
done
 
# PDF format conversion
echo "*  Convert PDF to TEXT by pdftotext (XPDF)."
cd $WRK
for i in `find -L . -newer $STP -name "*.pdf"`
do
    $PDFTOTEXT -enc UTF-8 -nopgbrk $SRC/$i $i.txt
    if [ $? -eq 0 ]; then
        echo "**  $PDFTOTEXT $i OK."
        $UCK < $i.txt > $i
        if [ $? -eq 2 ]; then
            echo "**  $i is empty. Ignore."
            rm -f $i
        fi
    else
        echo "**  $PDFTOTEXT $i NG. Ignore."
        rm -f $i
    fi
    rm -f $i.txt
done
 
# add BOM to text files
echo "*  Add BOM to TEXT files."
cd $WRK
for i in `find -L . -newer $STP -name "*.txt" -or -name "*.tex"`
do
    $UCK < $i > $i.tmp
    if [ $? -eq 2 ]; then
        echo "**  $i is empty. Ignore."
        rm -f $i
    fi
    mv $i.tmp $i
done
 
# rm svn control
cd $WRK
find . -name ".svn" -or -name ".#*" | xargs rm -fr
 
# Indexing
echo "*  Execute Indexing by genindex.pl. PARAM:"
echo "*   1 インデックス名前:        default"
echo "*   2 インデックス対象DIR:     $WRK"
echo "*   3 インデックス対象URL:     http://yasuda.homeip.net/"
echo "*   4 インデックス対象拡張子:   .html,.txt,.pdf,.tex"
echo "*   5 非インデックス対象DIR:   admin,common,css,archives"
echo "*   6 非インデックス対象拡張子: (指定無し)"
echo "*   7 非インデックス対象KWD:   (指定無し)"
echo "*   8 ランキング方法:         最終更新日時降順(1)"
echo "*   9 alt属性の文字:         指定しない(0)"
cd $MSE
./genindex.pl <<EOM
default
/home/isao/var/webindex/website
http://yasuda.homeip.net/
.html,.txt,.pdf,.tex
admin,common,css,archives
 
 
1
0
EOM
 
if [ $? -eq 0 ]; then
    echo "*  Index Generation Succeeded."
    ls -l $MSE/default.idx
    echo "*  Now Copy default.idx to $CGI."
    cp -p $MSE/default.idx $CGI
    # STAMP modify
    SDT=`ls -l $STP`
    echo "*  Previous Update: $SDT"
    touch $STP
    SDT=`ls -l $STP`
    echo "*  Now Updated:     $SDT"
else
    echo "*  Index Generation Something bad."
    ls -l $MSE/default.idx
fi
 
echo "*  Procedure Ended. `date '+%Y/%m/%d %H:%M:%S.'`"
# end of script