Apache Luceneとの連携
Zend_Search_Luceneで利用している索引は、本家であるApache Luceneと互換性があり、相互に利用することが可能です。
ここではApache Luceneで日本語を対象としたの索引の生成と、それのZend_Search_Luceneからの利用についての方法の一例について紹介します。
Zend_Search_Luceneでは索引のバージョン1.4から1.9、2.1と2.3を利用できます。このうち、索引バージョン1.4から1.9は読み込みのみ、2.1と2.3については読み込みと出力ができます。また、読み込んだ低いバージョンの索引を高いバージョンの形式で書き出すこともできます。ただし、これは元に戻せない(例えばバージョン2.1で読み込んだ索引をバージョン2.3で書き出してしまった場合、バージョン2.1には戻すことはできない)ので注意してください。
なお、Apache Luceneの最新版は2.4.1となっていますが、Zend Framework 1.7.7現在、これで作成した索引には対応していません。
日本語を扱うことが可能な拡張Lucene-jaで索引を作成した後、それをZend_Search_Luceneで検索する例について紹介します。
Lucene-jaを利用した索引の生成
Apache LuceneはJavaで実装されているテキスト検索エンジンで、Zend_Search_Luceneからすると本家にあたります。Apache Lucene自体には日本語のを解釈する機能はないのですが、容易に拡張可能であるため、日本語の文章を解釈するための方法がいくつか実装されています。ここではそのうちLucene-jaのCJKAnalyzerを利用して索引を作成する方法を簡単に紹介します。
CJKAnalyzerはbigramと呼ばれるモデルでテキストを解析します。これは文書に含まれる文字列を2文字ずつの組にして、その組を一つの単語のように見たてて索引に登録する手法です。例えば「この文字列」という文字列からは「この」「の文」「文字」「字列」の4つの組が抽出されることになります。この方法は単純で、精度が上がりにくいという面がある一方で、辞書を必要としないため日本語以外の言語で利用したり、辞書に登録されていないような単語なども検索対象とすることができるといった強みがあります。
それでは、実際にやってみましょう。まず、Lucene-jaはプロジェクトのサイトからダウンロードできます。
ここにある「lucene-ja-2.0test2.zip」をダウンロードし、展開してください。今回は展開された先のlibにある、「lucene-core-2.0.0.jar」と「lucene-ja.jar」を利用します。
これらのファイルとサンプルに含まれる「MakeCJKIndex.java」を同じフォルダに置いた上で、以下のコマンドを実行してください。
このMakeCJKIndexは引数で与えられたフォルダ内にあるテキストを対象に「codezine-index」という名前の索引を作成します。この索引の名前を変更したい場合には15行目にある変数index_nameの値を「codezine-index」から変更してください。
> javac -classpath lucene-core-2.0.0.jar;lucene-ja.jar;lucese-demos-2.0.0.jar;. MakeCJKIndex.java (コンパイル) > java -classpath lucene-core-2.0.0.jar;lucene-ja.jar;lucese-demos-2.0.0.jar;. MakeCJKIndex txt (txtフォルダに対して索引を作成) txt/1.txt txt/3.txt txt/2.txt txt/4.txt
なお、テキストファイルの内容は次のとおりであるとします。
txt/1.txt:これはcodezineのテストです。 txt/2.txt:うまくいっているかな?どんな文字だと検索できるかな? txt/3.txt:ちなみに文字コードはUTF-8です。お間違いなきよう。 txt/4.txt:英語がmixedでも、うまくparseできます。
ここまでで、日本語を含んだテキストファイルから索引を作成することができました。
この時点で「codezine-index」という名前のフォルダが作成されているはずです。この中に索引のファイルが格納されています。
CJKIndexを利用して生成した索引の検索
次に、この作成した索引から検索を行ってみましょう。今作成した索引に対し、「parse mixed」「でき」「できる」を検索してみる例を見てみましょう。
<?php
require_once 'Zend/Search/Lucene.php';
require_once 'My_Analyzer.php';
// (1) - テキスト解析オブジェクトの設定
Zend_Search_Lucene_Analysis_Analyzer::setDefault(
new My_Analyzer());
//次だと駄目
//Zend_Search_Lucene_Analysis_Analyzer::setDefault(
// new Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8());
// (2) - 索引の読み込み
$index = Zend_Search_Lucene::open('codezine-index');
function query($index, $string) {
// (3) - クエリの作成
$query = Zend_Search_Lucene_Search_QueryParser::parse($string);
// (4) - クエリの実行
$hits = $index->find($query);
// (5) - 結果の表示
echo "`".$string."'を検索した結果:\n";
foreach ($hits as $hit) {
echo $hit->score."\t".$hit->path."\n";
}
echo "\n";
}
query($index, 'parse mixed');
query($index, 'でき');
query($index, 'できる');
この例では(1)で文字列解析オブジェクトにMy_Analyzerを指定しています。これは、UTF-8を解釈するものを指定する(コメントアウトしている例)だけでは駄目なのでしょうか? コメントアウトをしている部分を実行すると、結果はリスト11のようになります。
% php query_ja.php `parse mixed'を検索した結果: 0.74827240807551 txt/4.txt `でき'を検索した結果: 0.40240064764118 txt/4.txt 0.24144038858471 txt/2.txt `できる'を検索した結果:
このうち「parse mixed」と「でき」については問題なく検索できていますが、「txt/2.txt」に含まれているはずの「できる」が検索されていません。これは、先程作成した索引に登録されているのがbi-gramであるため、「できる」は「でき」と「きる」の2つの単語に分かれて登録されている一方で、検索する方は「できる」という単語を探してしまっているための失敗です。
これを解決するためには、与えられた文字列をbi-gramに切る処理を行う文字列解析のオブジェクトを作成し、利用してやる必要があります。ここではZend_Search_Lucene_analysis_Analyzer_Common_Utf8を継承したMy_Analyzerを作ることにします。
My_AnalyzerとZend_Search_Lucene_Analysis_Analyzer_Common_Utf8の主な違いをリスト12に示します。
<?php
require_once 'Zend/Search/Lucene/Analysis/Analyzer/Common/Utf8.php';
class My_Analyzer extends Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8
{
...
public function nextToken()
{
...
do {
$bigram = false;
$bigram_first_letter = null;
$matchedWord = null;
/* (1) アルファベットの場合は単語ごと */
if (preg_match('/[\p{Ll}\{Lu}]+/u', $this->_input, $match, PREG_OFFSET_CAPTURE, $this->_bytePosition)) {
$matchedWord = $match[0][0];
//ascii charater
}
/* (2) それ以外の場合はbi-gramを得る */
else if (preg_match('/([\p{L}])([\p{L}])/u', $this->_input, $match, PREG_OFFSET_CAPTURE, $this->_bytePosition)) {
$bigram = true;
$matchedWord = $match[0][0];
$bigram_first_letter = $match[1][0];
//two characters
}
...
/* (3)何文字分読み進めるか */
if (!$bigram) {
$this->_bytePosition = $binStartPos + strlen($matchedWord);
$this->_position = $endPos;
} else {
$this->_bytePosition = $binStartPos + strlen($bigram_first_letter);
$this->_position = $startPos + iconv_strlen($bigram_first_letter, 'UTF-8');
}
} while ($token === null); // try again if token is skipped
...
}
}
このように、My_AnalyzerではnextTokenメソッドを書き換えています。まず、アルファベットのみの単語を見つけた場合(1)は通常どおり、その単語を抽出しています。一方でアルファベット以外の文字を含んでいる場合(2)には、連続する2つの文字からbi-gramを抽出しています。また、何文字分読み進めるかについても、bi-gramを抽出した場合には1文字分しか読み進まないように変更してあります。
例えば「英語がmixedでも、うまくparseできます。」という文章は図1のように解析されます。

図1では青色の矢印が現在解析している点、緑色の矢印と茶色の矢印が次に解析する点を示しています。茶色の枠で囲まれているところが(1)のルールで切り出された部分、緑色の枠で囲まれているところが(2)のルールで切り出された部分になっています。
(2)ではbi-gram内の1文字目と2文字目とを正規表現「([\p{L}])([\p{L}])」で別々に切り出しています。これは(3)で1文字分だけ解析位置を移動するため1文字目を別に保存しておく必要があるからです。
このMy_Analyzerを利用した検索を行うスクリプトをリスト13に示します:
<?php
require_once 'Zend/Search/Lucene.php';
require_once 'My_Analyzer.php';
// (1) - テキスト解析オブジェクトの設定
Zend_Search_Lucene_Analysis_Analyzer::setDefault(
new My_Analyzer());
// (2) - 索引の読み込み
$index = Zend_Search_Lucene::open('codezine-index');
function query($index, $string) {
// (3) - クエリの作成
$query = Zend_Search_Lucene_Search_QueryParser::parse($string);
// (4) - クエリの実行
$hits = $index->find($query);
// (5) - 結果の表示
echo "`".$string."'を検索した結果:\n";
foreach ($hits as $hit) {
echo $hit->score."\t".$hit->path."\n";
}
echo "\n";
}
query($index, 'parse mixed');
query($index, 'でき');
query($index, 'できる');
これを実行した結果がリスト14です:
% php query_ja.php `parse mixed'を検索した結果: 0.74827240807551 txt/4.txt `でき'を検索した結果: 0.40240064764118 txt/4.txt 0.24144038858471 txt/2.txt `できる'を検索した結果: 0.39884526905006 txt/2.txt
無事に「txt/2.txt」が見つかっていることが分かります。
おわりに
今回はテキスト検索エンジンのZend_Search_Luceneモジュールの詳細について説明し、またJavaベースのApache Luceneとの索引の共有について説明しました。
次回はAdobeのFlash Playerとの間のデータをやりとりするための仕組みZend_Amfについて紹介したいと思います。
