symfonyとHyperEstraierを使って全文検索してみよう

はじめまして。インターンのsato(@hilotter)です。

突然ですが、みなさんは「全文検索エンジン」って使ったことがありますか?
「全文検索エンジン」と聞くと「何だか難しそうだな」と思われる方もいらっしゃると思います。
僕はまさにそうでした。

そんな全文検索エンジン初心者の僕のもとに、今回、HyperEstraierという全文検索エンジンを使ってキーワード検索機能を実装する機会がありました。

色々調べてみたのですがsymfonyとHyperEstraierを使って検索を行う記事がなかったのでご紹介させていただきます。
間違い等ありましたらご指摘いただければ幸いです。

今回はサンプルとして簡単なキーワード検索機能を作ってみたいと思います。

なお、使用したシステムのバージョンは
symfony1.0(ORMはPropel)
HyperEstraier1.4.13
となっています。

目次

  1. HyperEstraier設定
  2. MySQLサンプルテーブル作成
  3. symfonyからHyperEstraierを利用

1.HyperEstraier設定

インストール

ここではCentOSでのインストール方法について説明します。
HyperEstraierはlibiconvzlibQDBMというライブラリを利用しているので、まずはこれらをインストールし、その後、HyperEstraierのインストールを行います。

  • libiconvのインストール
  • sudo yum -y install libiconv-devel
    
  • zlibのインストール
  • sudo yum -y install zlib-devel
    
  • QDBMのインストール
  • 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
    
  • HyperEstraierのインストール
  • 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

を入力します。

作成したノードと管理画面へのリンクが表示されています。

symfony-hyperestraier-admintop.png

ユーザ名がデフォルトのままだとよくないので、administration -> Manage Usersにアクセスして任意のユーザを作成しましょう。

symfony-hyperestraier-usercreate.png

左から順に、ユーザ名、パスワード、権限(sを付けると管理者ユーザ)、名前、備考となっています。
テスト用ユーザを作成します。

test    test    s     AdminTest    Sample

ユーザ追加後、adminを削除しページをリロードします。
再度認証が行われるので作成したユーザ名とパスワードを入力します。

これでHyperEstraierの基本設定が完了しました。
次はサンプル用データをDBに登録します。

2.MySQLサンプルテーブル作成

サンプルテーブル作成

キーワードによる商品検索を行うため、以下のカラムを持つテーブルを作成します。

  • id
  • タイトル
  • 本文
  • キャッチコピー
  • 価格
  • 作成日時
  • 更新日時

CREATE TABLE test_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/Services

HyperEstraierの設定情報を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; ?>

実際の画面

作成したキーワード検索を試してみます。

「アプリ」で検索すると、全ての商品のキャッチコピーにアプリというキーワードが含まれているので全商品が表示されます。

symfony-hyperestraier-searchresult_1.png

「アプリ カメラ」で検索するとAND検索となり、アプリとカメラというキーワードを持つ構図カメラのみが表示されます。

symfony-hyperestraier-searchresult_2.png

キーワード検索、動いてます。

参考記事

全文検索システム Hyper Estraier
Hyper Estraier で検索
Page2

まとめ

まだまだ細かい設定等を行う必要があると思いますが、
簡単なキーワード検索であれば全文検索エンジン初心者の僕でも作る事ができました。
「難しそう」というイメージで、行動するのをためらってしまうのではなく
「とりあえずやってみる」ことが大事だと改めて感じました。

カヤックでは全文検索エンジンを愛してやまない技術者も募集しています!