はじめまして。インターンのsato(@hilotter)です。
突然ですが、みなさんは「全文検索エンジン」って使ったことがありますか?
「全文検索エンジン」と聞くと「何だか難しそうだな」と思われる方もいらっしゃると思います。
僕はまさにそうでした。
そんな全文検索エンジン初心者の僕のもとに、今回、HyperEstraierという全文検索エンジンを使ってキーワード検索機能を実装する機会がありました。
色々調べてみたのですがsymfonyとHyperEstraierを使って検索を行う記事がなかったのでご紹介させていただきます。
間違い等ありましたらご指摘いただければ幸いです。
今回はサンプルとして簡単なキーワード検索機能を作ってみたいと思います。
なお、使用したシステムのバージョンは
symfony1.0(ORMはPropel)
HyperEstraier1.4.13
となっています。
目次
1.HyperEstraier設定
インストール
ここではCentOSでのインストール方法について説明します。
HyperEstraierはlibiconv、zlib、QDBMというライブラリを利用しているので、まずはこれらをインストールし、その後、HyperEstraierのインストールを行います。
- libiconvのインストール
sudo yum -y install libiconv-devel
sudo yum -y install zlib-devel
wget http://qdbm.sourceforge.net/qdbm-1.8.77.tar.gz gunzip ./qdbm-1.8.77.tar.gz tar -xf ./qdbm-1.8.77.tar rm -f ./qdbm-1.8.77.tar cd ./qdbm-1.8.77/ ./configure --enable-zlib make sudo make install
wget http://hyperestraier.sourceforge.net/hyperestraier-1.4.13.tar.gz gunzip ./hyperestraier-1.4.13.tar.gz tar -xf ./hyperestraier-1.4.13.tar rm -f ./hyperestraier-1.4.13.tar cd ./hyperestraier-1.4.13 ./configure make sudo make installこれでHyperEstraierのインストールが完了しました。
CentOS以外のOSをご利用の場合は公式マニュアルのインストール方法をご参照ください。
P2P機構に関する初期設定
公式マニュアルによると「Hyper Estraierの真価は、そのP2P機構にあります。」とのこと。
C/S(クライアント/サーバ)方式を用いる事で、通常の「estcmd」を用いた場合にはできない、
検索と更新が並列に行えたり、複数のインデックスを扱えたりするそうです。
とても便利そうなのでP2P機構を使ってみたいと思います。
symfonyのルートディレクトリ(今回は/var/www/html/test)に移動し、以下のコマンドを実行します。
ノードマスタをdataフォルダ以下に作成 estmaster init data/hyperestraierノードの作成 estcmd create data/hyperestraier/_node/sample
ノードマスタ起動 estmaster start -bg data/hyperestraier/ -bgオプションをつけることでバックグラウンドで動いてくれます
ノードマスタを起動した後ブラウザから
http://ホスト名:1978/
にアクセスするとノード管理画面にアクセスできます。
認証画面ではデフォルトユーザである
ID:admin Pass:admin
を入力します。
作成したノードと管理画面へのリンクが表示されています。
ユーザ名がデフォルトのままだとよくないので、administration -> Manage Usersにアクセスして任意のユーザを作成しましょう。
左から順に、ユーザ名、パスワード、権限(sを付けると管理者ユーザ)、名前、備考となっています。
テスト用ユーザを作成します。
test test s AdminTest Sample
ユーザ追加後、adminを削除しページをリロードします。
再度認証が行われるので作成したユーザ名とパスワードを入力します。
これでHyperEstraierの基本設定が完了しました。
次はサンプル用データをDBに登録します。
2.MySQLサンプルテーブル作成
サンプルテーブル作成
キーワードによる商品検索を行うため、以下のカラムを持つテーブルを作成します。
- id
- タイトル
- 本文
- キャッチコピー
- 価格
- 作成日時
- 更新日時
CREATE TABLEtest_product
(id
INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,title
VARCHAR(256) NULL ,text
TEXT NULL ,catch_copy
TEXT NULL ,price
INT NOT NULL ,created_at
DATETIME NULL ,updated_at
DATETIME NULL ) ENGINE = InnoDB;+------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | title | varchar(256) | YES | | NULL | | | text | text | YES | | NULL | | | catch_copy | text | YES | | NULL | | | price | int(11) | NO | | NULL | | | created_at | datetime | YES | | NULL | | | updated_at | datetime | YES | | NULL | | +------------+--------------+------+-----+---------+----------------+
テスト用データの登録
サンプルとして、BM11の99プロジェクト達成&解散記念セール!で販売されている商品をいくつか登録してみます。
INSERT INTO test_product (title, text, catch_copy, price, created_at, updated_at) VALUES ('構図カメラ', 'とてもベーシックな写真の構図をわかりやすくグリッドや人型のテンプレートで表示し、フレームの中で何をどのように配置したら魅力的に映るかを解説つきでサポートします。', 'いつもの写真を少し素敵にするiPhoneのカメラアプリ', 230, now(), now()); INSERT INTO test_product (title, text, catch_copy, price, created_at, updated_at) VALUES ('キミの執事', 'あなただけの執事が居るとしたら...執事にどんなことを求めますか?新感覚、執事育成ゲーム「キミの執事」がモバイルmixiアプリで登場!あなただけの執事を雇い、おしゃべりしよう!', '1on1人口無能型 モバイルmixiアプリ', 1000000, now(), now()); INSERT INTO test_product (title, text, catch_copy, price, created_at, updated_at) VALUES ('タイムミラー', '1 〜 10 秒前までの過去を映す鏡です。また画面を 1 〜 64 まで分割して過去の軌跡を映すこともできます。使い方は自由自在!鏡の前で回って頭の後ろの寝癖をチェックしたり、普段確認できない自分の動き (ゴルフのスイングなど) を見てみたりすることができます。', '10 秒の時間旅行を楽しめる AIR アプリ!', 200000, now(), now());
これでサンプルデータを登録することができました。
次はいよいよsymfonyからHyperEstraierを利用してみます。
3.symfonyからHyperEstraierを利用
モデルの作成
まずは先ほど作成したテーブルに対するモデルを作成します。
DBからスキーマを作成 symfony propel-build-schemaモデルの作成 symfony propel-build-model
ドラフト文章の設定
続いて、インデックスの作成を行います。
今回はMySQLのテーブルからインデックスを作成するので、文章ドラフト形式を利用します。
文章ドラフトはHyperEstraierの独自のデータ形式で、任意のデータをインデックスとして登録できます。
今回作成したドラフト文章は
@url=sample:product:ID @title=タイトル @cdate=作成日(ISO 8601形式) @mdate=更新日時(ISO 8601形式) [改行] 本文 [タブ]タイトル [タブ]キャッチコピーという形式です。
これにより本文、タイトル、キャッチコピーを対象としてキーワード検索が行えます。
また、タブで始まっている部分は隠しテキストとして扱うことができます。
隠しテキストにすると検索対象にはなりますが検索結果のスニペットでは表示されなくなります。
ノードAPIの設置
文章ドラフトをインデックスに登録するためには、ノードAPIを用います。
このノードAPIですが、公式マニュアルにはPHP版は存在しませんでした。
調べてみるとServices_HyperEstraierというPHP用クラスライブラリを公開されている方がいらっしゃったので、そちらを使わせていただくことにしました。Page2さんありがとうございます。
解凍して得られたServicesディレクトリをlibディレクトリ以下に設置 lib/ServicesHyperEstraierの設定情報をapp.ymlに記述 config/app.yml
all: hyperestraier: uri: http://localhost:1978/node/sample user: test pass: test
Services_HyperEstraierに含まれていたサンプルファイルを参考にインデックス登録プログラムと検索プログラムを作成します。
インデックス登録用バッチの作成
batch/register_index.php<?php
symfonyバッチ処理初期設定略require_once 'Services/HyperEstraier/Node.php'; //HyperEstraier設定情報をapp.ymlから取得 $uri = sfConfig::get('app_hyperestraier_uri'); $user = sfConfig::get('app_hyperestraier_user'); $pass = sfConfig::get('app_hyperestraier_pass');
// create and configure the node connecton object $node = new Services_HyperEstraier_Node; $node->setUrl($uri); $node->setAuth($user, $pass);
//Sampleテーブルからデータ取得 $lists = TestProductPeer::doSelect(new Criteria()); foreach ($lists as $list) { $doc = $node->getDocumentByUri('sample:product:' . $list->getId());
//インデックス登録済みの場合、テーブルのデータが //インデックスの更新日時より新しければインデックスを更新 if (!is_null($doc)) { $mdate = $doc->getAttribute('@mdate'); if (strtotime($list->getUpdatedAt()) > strtotime($mdate)) { registerIndex($list, $node); } } //インデックス登録されていないデータの場合、新規登録 else { registerIndex($list, $node); }
}
function registerIndex($list, $node) { $doc = new Services_HyperEstraier_Document; $doc = settingDoc($list, $doc); if (!$node->putDocument($doc)) { fprintf(STDERR, "error: %d\n", $node->status); if (Services_HyperEstraier_Error::hasErrors()) { fputs(STDERR, print_r(Services_HyperEstraier_Error::getErrors(), true)); } } }
function settingDoc($list, $doc) { //属性の設定 $doc->addAttribute('@uri', 'sample:product:' . $list->getId()); $doc->addAttribute('@title', $list->getTitle()); $doc->addAttribute('@cdate', date('c', strtotime($list->getCreatedAt()))); $doc->addAttribute('@mdate', date('c', strtotime($list->getUpdatedAt())));
//本文登録 $doc->addText($list->getText()); $doc->addHiddenText($list->getTitle()); $doc->addHiddenText($list->getCatchcopy()); return $doc;
}
バッチ作成後、以下のコマンドを実行し、インデックスの登録を行います。
php batch/register_index.phpこれでインデックスの登録ができました。
テストモジュールの作成
symfony init-module front test
キーワード検索用のアクションを作成
※ executeIndex()内に関してですが、Services_HyperEstraierライブラリの仕様でStrictStandardsメッセージが表示されるため、一時的にエラーレベルを下げる処理を行っています。
apps/fromt/modules/test/actions/actions.class.php<?php class testActions extends sfActions { public function executeIndex() { //StrictStandardsメッセージを非表示に $E = error_reporting(); if(($E & E_STRICT) == E_STRICT) error_reporting($E ^ E_STRICT);
$this->products = ''; if($this->getRequestParameter('keyword')) { //キーワードを含む商品IDを取得 $ids = $this->getIdsFromKeyword($this->getRequestParameter('keyword')); //商品IDから商品データを取得 $this->products = TestProductPeer::getProductsFromIds($ids); } error_reporting($E);
}
private function getIdsFromKeyword($keyword) { require_once 'Services/HyperEstraier/Node.php'; $uri = sfConfig::get('app_hyperestraier_sample_uri'); //ノードオブジェクトの作成 $node = new Services_HyperEstraier_Node; $node->setUrl($uri);
//検索用オブジェクトの作成 $cond = new Services_HyperEstraier_Condition; $cond->setPhrase($keyword); //キーワードを設定 $cond->setOptions(Services_HyperEstraier_Condition::SIMPLE); //簡易検索モードに設定 $cond->setSkip(0); $nres = $node->search($cond, 0); //第二引数はメタ検索時の深さ $productIds = array(); $docnum = $nres->docNum(); if ($docnum != 0) { for ($i = 0; $i < $docnum; $i++) { $rdoc = $nres->getDocument($i); //URLに登録されている商品IDを配列に格納 if (($value = $rdoc->getAttribute('@uri')) !== null) { preg_match('/[0-9]+$/', $value, $match); array_push($productIds, $match[0]); } } } return $productIds;
} }
商品検索モデルの作成
lib/model/TestProductPeer.php<?php class TestProductPeer extends BaseTestProductPeer { public static function getProductsFromIds($ids) { $c = new Criteria(); $c->add(self::ID, $ids, Criteria::IN); return self::doSelect($c); } }
ビューの作成
apps/fromt/modules/test/templates/indexSuccess.php<form> <input type="text" name="keyword" value="" /> <input type="submit" value="検索" /> </form><?php if($products): ?> <p>「<?php echo $sf_request->getParameter('keyword'); ?>」の検索結果</p> <table border="1" > <tr> <th width="100px">商品名</th> <th width="200px">キャッチコピー</th> <th>本文</th> <th width="100px">価格</th> </tr> <?php foreach($products as $product): ?> <tr> <td><?php echo $product->getTitle() ?></td> <td><?php echo $product->getCatchCopy() ?></td> <td><?php echo $product->getText() ?></td> <td><?php echo $product->getPrice() ?>円</td> </tr> <?php endforeach; ?> </table> <?php else: ?> <p>該当する商品はありません。</p> <?php endif; ?>
実際の画面
作成したキーワード検索を試してみます。
「アプリ」で検索すると、全ての商品のキャッチコピーにアプリというキーワードが含まれているので全商品が表示されます。
「アプリ カメラ」で検索するとAND検索となり、アプリとカメラというキーワードを持つ構図カメラのみが表示されます。
キーワード検索、動いてます。
参考記事
全文検索システム Hyper Estraier
Hyper Estraier で検索
Page2
まとめ
まだまだ細かい設定等を行う必要があると思いますが、
簡単なキーワード検索であれば全文検索エンジン初心者の僕でも作る事ができました。
「難しそう」というイメージで、行動するのをためらってしまうのではなく
「とりあえずやってみる」ことが大事だと改めて感じました。