GANCHIKU.com

symfonyで全文検索でもしてみるか。

2008年2月19日
PHP

WEB+DB Pressを今回の外こもり合宿のお供に連れていった際に、ニコ動の記事が載っていたので勉強をさせてもらった。特に、負荷対策について勉強になったな。私は、今まで作ってきた仕組みはそこまで負荷対策に敏感にならずに開発してきたので、私の管轄外だよなー、なんて思っていたのだけども、そんなことをいつまでも言ってられない状態なので調べてやってみた。つーか、負荷対策が必要なくらいなシステムに従事してみたい。。。ウレシイ悲鳴ってやつか。

ニコ動でも使用しているように、Senna+MySQL(tritonn)を採用してみた。面倒だなと思ったのは、次のところ。
1:Senna+MySQL(tritonn)では、MyISAMを指定しなければいけない。他は、InnoDBを採用するのに。
2:検索結果をページングしたいときは、sfAdvancedPropelPagerを採用しなければいけない。
3:summaryテーブルを作成した方が良い。

1:Senna+MySQL(tritonn)では、MyISAMを指定しなければいけない。他は、InnoDBを採用するのに。

Senna+MySQL(tritonn)は、MyISAMのみの対応なのだけども、MyISAMは更新系に弱いそうで、ニコ動でも検索以外のところでは、InnoDBを採用しているようだった。これの何が面倒かと言うと、symfony propel-build-allとかで普通にモデルを作成したり、テーブルを作成すると、テーブルごとにMyISAMだとかInnoDBって変更することができないのね。たぶん。なので、一度、InnoDBで作成した後で、MyISAMに作りなおしてあげる必要があるのだよ。propel-build-allとかにhookをかけてあげればいいのだろうけど、やり方がわからなかったので、単純にSQLを流し込むことにした。

DROP TABLE IF EXISTS 'summaries';
CREATE TABLE summaries
(
 'id' integer not null auto_increment,
 'hoge_id' integer not null,
 'name' varchar(255),
 'descritption' text,
 'more_text' text,
 'more_more_text' text,
 PRIMARY KEY ('id'),
  FULLTEXT INDEX fulltext_index USING SECTIONALIZE (name, description, more_text, more_more_text)
)Type=MyISAM DEFAULT CHARSET = utf8;

とかで上書きしてあげる。この時点で面倒すぐる。で、SECTIONALIZEを指定することにより、複数のカラムを全文検索の対象にすることができるのだ。実際検索する際に、重みつけなんかをすればよし。

前後するけど。。。

3:summaryテーブルを作成した方が良い。

実際このテーブルはサマリーテーブルなのだけども、検索要素をテーブルをまたがって検索するのではなく、バッチ処理なんかで走らせたり、更新が起きたら走らせるなど方法で、サマリーテーブルを更新する必要がある。ニコ動は同期性をよりも、バッチ処理で速い検索をポリシーとして置いているので、このように対応したそうだ。また、askeetを読むと、更新時に更新したQuestionに関するindexを毎回更新しているみたいだけど、この辺もポリシーかな。

私もいろいろ考えたが、確かに、検索結果の反映に関しては、そこまで即時性は持つ必要がないと思うので、ニコ動の考えに賛成だな。それにJOINとかしているとやっぱり検索が重くなるそうなので、極力シンプルなSQLを発行する必要があるみたい。他のは、キャッシュでなんとかすればいいとは思うけど、検索結果をキャッシュはしないよね。

2:検索結果をページングしたいときは、sfAdvancedPropelPagerを採用しなければいけない。

まぁ、sfAdvancedProplePagerではなくてもいいのだけど、普通のsfPropelPagerではCriteriaを渡すことが無理なので、ごめんなさい。ここ訂正します。SQLからCriteriaを作成することができないので、sfPropelpagerにsetCriteriaで渡せないのです。独自の方法でSQLを渡す方法を考える必要がある。がんばったらそれ専用のCriteriaを作ることができそうな感じもするが、ここではsfAdvancedPropelPagerを採用した方が解決方法としていいと思う。

sfAdvancedPropelPagerは、Code Snippetsとして提供されている。http://www.symfony-project.org/snippets/snippet/119
しかし、こんなことが書いてある。

PLEASE NOTE: You will have to change all private methods and properties in sfPropelPager from ‘private’ to ‘protected’. This won’t break paging elsewhere in your projects, I promise you! What it will do is allow this subclass to gain access to it’s parent’s data and functionality. It should be noted that this addon was developed with the current stable version (0.6.3) of Symfony in mind.

sfPropelPagerを継承しているので、sfPropelPagerのprivateメソッドやプロパティをprotectedに書き換えないといけない、と書いてあった。しかしながら、今開発環境で使用しているsymfonyは、1.0.8なのだけども、protectedになっていたので、書き換える必要はない。0.6.3の場合かな。

というわけで、sfAdvancedPropelPager.class.phpとかに適当に名前を書いて、pathが通るところに置いてあげる。この置く場所が悩ましいんだよなぁ。。。私は、/lib/の下に置いてしまったけど、本当はどこに置くべきなの?

さて、使いかたは、次のような感じ。検索するHogePerrクラスとかにsearchとか言うメソッドでも置いてみて、そこで、sfAdvancedPropelPagerを使用するだけ。

    public static function search($word, $page = 1, $limit = 20)
    {
        $con = Propel::getConnection();
        $query = '
            SELECT * FROM summaries WHERE
            MATCH (name, description, more_text, more_more_text)
            AGAINST (? IN BOOLEAN MODE)
            ';
        $stmt = $con->prepareStatement($query);
        $priority = sprintf("W1:%s,2:%s,3:%s,4:%s",
            sfConfig::get('app_search_name'),
            sfConfig::get('app_search_description'),
            sfConfig::get('app_search_more_text'),
            sfConfig::get('app_search_more_more_text')
            );
        $stmt->setString(1, $priority . " " . $word);
        $pager = new sfAdvancedPropelPager('Summary', $limit);
        $pager->setStatement($stmt);
        $pager->setPage($page);
        $pager->init();
        return $pager;
    }

$priorityは、検索のスコアリングね。あとあといじりたいときのために、こうやってapp.ymlとかに書いておいて、調整をするといいかも。

templateの方では、sfPropelPagerと使いかたは同じなので、言及はしない。次のページとかへのリンクを@search?q=クエリー文字列&p= $pager->getNextpage()とかにするだけ。

ちょっと凝ったことをするとやっぱりフレームワークって面倒やね。単純なCRUDだけの仕組みならいいのだけど、世の中で求められているものはもう少し複雑なんだよね。prototype.js以外のJavaScriptフレームワークをフレームワークで使用しようとすると、ヘルパー関数がなかったりして、採用しにくいし。sfCSRFPluginとかAjaxでcsrf attack detectedとか怒ってくるので、泥臭いことしないといけないし。

私は今まで全文検索使ったことがなかったが、ようやく全文検索を使う機会ができました。HE使っていなくて、師匠に顔向けができないけど。。。

Shin Ohno 2003-2012