SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

Zend Framework入門

Zend Framework入門(20):
PHPテキスト検索エンジン - Zend_Search_Luceneの詳細 -

Zend Frameworkによる実践的なPHPアプリケーション開発 20

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」から変更してください。

[リスト8]MakeCJKIndexのコンパイルと実行
> 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

  なお、テキストファイルの内容は次のとおりであるとします。

[リスト9]サンプルテキストファイルの中身
txt/1.txt:これはcodezineのテストです。
txt/2.txt:うまくいっているかな?どんな文字だと検索できるかな?
txt/3.txt:ちなみに文字コードはUTF-8です。お間違いなきよう。
txt/4.txt:英語がmixedでも、うまくparseできます。

 ここまでで、日本語を含んだテキストファイルから索引を作成することができました。

 この時点で「codezine-index」という名前のフォルダが作成されているはずです。この中に索引のファイルが格納されています。

CJKIndexを利用して生成した索引の検索

 次に、この作成した索引から検索を行ってみましょう。今作成した索引に対し、「parse mixed」「でき」「できる」を検索してみる例を見てみましょう。

[リスト10]サンプルテキストファイルの中身
<?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のようになります。

[リスト11]My_Analyzerの代わりにAnalyzer_Common_Utf8を使った場合
% 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に示します。

[リスト12]bi-gramに切る処理をするMy_Analyzer(My_Analyzer.php)
<?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 「英語がmixedでも、うまくparseできます。」の解析
図1 「英語がmixedでも、うまくparseできます。」の解析

 図1では青色の矢印が現在解析している点、緑色の矢印と茶色の矢印が次に解析する点を示しています。茶色の枠で囲まれているところが(1)のルールで切り出された部分、緑色の枠で囲まれているところが(2)のルールで切り出された部分になっています。

 (2)ではbi-gram内の1文字目と2文字目とを正規表現「([\p{L}])([\p{L}])」で別々に切り出しています。これは(3)で1文字分だけ解析位置を移動するため1文字目を別に保存しておく必要があるからです。

 このMy_Analyzerを利用した検索を行うスクリプトをリスト13に示します:

[リスト13]bi-gramの索引を検索するスクリプト(query_ja.php)
<?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です:

[リスト14]My_Analyzerを利用して検索した結果
% 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について紹介したいと思います。

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
Zend Framework入門連載記事一覧

もっと読む

この記事の著者

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

WINGSプロジェクト 風田 伸之(カゼタ ノブユキ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。 2026年時点での登録メンバは約50名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/3947 2009/06/15 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー