CakePHP + mroonga
麺処まつば副店長です。
あけましておめでとうございます。(遅)
年末年始から今まで、当店はモンハン一色でございます。
店長も副店長も働きもせずモンハンばっかりやっています。(半ば開き直りながら)
店長が遊んでいる=自分も遊んでいい。とか思ってダラダラしていたら
ついに(自分だって遊んでばっかりの)店長に「そろそろ書け」とか言われました…。
なんかネタあったっけな……と(遊んでばっかりの)記憶を掘り起こしてみたところ、
先日別件で CakePHP で全文検索したいから mroonga を動かせるようにしといて。
というお仕事があったのです。
CakePHP は、mroonga に対応してないので
オーバーライドして無理矢理対応させた時の話でもしようと思います。
まず、CakePHP?mroonga?なにそれ美味しいの?って方はコチラへ。
CakePHP:http://cakephp.jp/
mroonga:http://mroonga.github.com/
<使用環境>
CakePHP:2.0
mroonga:1.2
mysql:5.5
CentOS 5.7
今回書くのは下記3つです。
1.cake schema create
2.cake schema generate
3.find
では書いてみます。
cake schema create に対応する。
mroonga な全文検索に対応したテーブルを作成するには、
通常下記のような SQL でテーブルを作成します。
1 CREATE TABLE diaries ( 2 id INT(10) NOT NULL AUTO_INCREMENT, 3 title VARCHAR(255) NOT NULL, 4 content VARCHAR(255) NOT NULL, 5 FULLTEXT INDEX (title), 6 FULLTEXT INDEX (content), 7 PRIMARY KEY (`id`) 8 ) ENGINE = mroonga COMMENT = 'engine "innodb"'
<5、6行目>
title カラムと content カラムを FULLTEXT INDEX に対応させています。
<8行目>
エンジンに mroonga を使用する宣言をしています。
さらに、今回はラッパーモードを使用するので、
テーブルのコメントに「engine "innodb"」を入れる必要があります。
コレを実現するために、こういう schema.php を書きます。
<?php 1 public $diaries = array( 2 'id' => array('type' => 'integer', 'null' => false, 'default' => NULL, 'key' => 'primary', 'collate' => NULL, 'comment' => ''), 3 'title' => array('type' => 'string', 'null' => true, 'default' => NULL, 'key' => 'index', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), 4 'content' => array('type' => 'string', 'null' => true, 'default' => NULL, 'key' => 'index', 'collate' => 'utf8_general_ci', 'comment' => '', 'charset' => 'utf8'), 5 'indexes' => array( 6 'PRIMARY' => array('column' => 'id', 'unique' => 1), 7 'title' => array('column' => 'title', 'unique' => 0, 'index_type' => 'FULLTEXT'), 8 'content' => array('column' => 'content', 'unique' => 0, 'index_type' => 'FULLTEXT'), 9 ), 10 'tableParameters' => array('charset' => 'utf8', 'collate' => 'utf8_general_ci', 'engine' => 'mroonga', 'comment' => 'engine "innodb"') 11 );
<7、8行目>
FULLTEXT INDEX (title),」と「FULLTEXT INDEX (content),」に該当する行です。
<10行目>
「ENGINE = mroonga COMMENT = 'engine "innodb"'」に該当する行です。
さらにこれを、schema createで実行できるようにオーバーライドします。
まずは、ラッパーモード使用の宣言をするため、テーブルにコメントが付けられるようにします。
対象ファイル:project_name/lib/Cake/Model/Datasource/Database/Mysql.php
<?php 1 public $tableParameters = array( 2 'charset' => array('value' => 'DEFAULT CHARSET', 'quote' => false, 'join' => '=', 'column' => 'charset'), 3 'collate' => array('value' => 'COLLATE', 'quote' => false, 'join' => '=', 'column' => 'Collation'), 4 'engine' => array('value' => 'ENGINE', 'quote' => false, 'join' => '=', 'column' => 'Engine') 5 // kokokara ------------------------------------------ 6 ,'comment' => array('value' => 'COMMENT', 'quote' => true, 'join' => '=', 'column' => 'Comment') 7 // kokomade ------------------------------------------ );
6行目を追加します。
これで schema.php 中にテーブルの「comment」があった場合の処理を追加します。
続いて、FULLTEXT INDEX宣言できるようにします。
対象ファイル:project_name/app/Model/Datasource/DboSource.php
buildIndex 関数をオーバーライドします。10〜14行目追記。
<?php 1 public function buildIndex($indexes, $table = null) { 2 $join = array(); 3 foreach ($indexes as $name => $value) { 4 $out = ''; 5 if ($name === 'PRIMARY') { 6 $out .= 'PRIMARY '; 7 $name = null; 8 } else { 9 // kokokara ------------------------------------------- 10 if(isset($value['index_type']) && $value['index_type'] == 'FULLTEXT' ){ 11 $out = 'FULLTEXT INDEX('.$value['column'].')'; 12 $join[] = $out; 13 continue; 14 } 15 // kokomade ------------------------------------------- 16 if (!empty($value['unique'])) { 17 $out .= 'UNIQUE '; 18 } 19 $name = $this->startQuote . $name . $this->endQuote; 20 } …略…
この状態で、先程の schema.php を走らせてみます。
$ app/Console/cake schema create
対象テーブルがどうなったかを確認します。
SHOW CREATE TABLE diaries; CREATE TABLE `diaries` ( `id` int(10) NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `content` varchar(255) NOT NULL, PRIMARY KEY (`id`), FULLTEXT KEY `title` (`title`), FULLTEXT KEY `content` (`content`) ) ENGINE=mroonga DEFAULT CHARSET=utf8 SHOW INDEX FROM diaries; +---------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +---------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | diaries | 0 | PRIMARY | 1 | id | A | 0 | NULL | NULL | | BTREE | | | | diaries | 1 | title | 1 | title | NULL | NULL | NULL | NULL | | FULLTEXT | | | | diaries | 1 | content | 1 | content | NULL | NULL | NULL | NULL | | FULLTEXT | | | +---------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
できたっぽいです。
cake schema generate に対応する
上記までで、schema createはできるようになったので、その逆です。
schema generate で schema.php を出力できるようにします。
→(2012/03/21)副店長の記憶が非常に曖昧なため一旦削除します。
→(2012/04/06)副店長の記憶は正しかったです。戻します。
対象ファイル:project_name/lib/Cake/Model/Datasource/Database/Mysql.php
<?php 1 public function index($model) { 2 $index = array(); 3 $table = $this->fullTableName($model); 4 $old = version_compare($this->getVersion(), '4.1', '<='); 5 if ($table) { 6 $indices = $this->_execute('SHOW INDEX FROM ' . $table); 7 while ($idx = $indices->fetch()) { 8 if ($old) { 9 $idx = (object) current((array)$idx); 10 } 11 // kokokara ------------------------------------------- 12 if($idx->Index_type == "FULLTEXT"){ 13 $col = array(); 14 $index[$idx->Key_name]['column'] = $idx->Column_name; 15 $index[$idx->Key_name]['unique'] = intval($idx->Non_unique == 0); 16 $index[$idx->Key_name]['index_type'] = "FULLTEXT"; 17 continue; 18 } 19 // kokomade ------------------------------------------- 20 if (!isset($index[$idx->Key_name]['column'])) { 22 $col = array(); 23 … 24 }
index 関数をオーバーライドします。
12〜18行目を追加。INDEXの定義に「FULLTEXT」があったら、
その内容を出力するように書き換えました。
この状態で、schema generate をすると、先程書いたような記述が schema.php に出力されます。
CakePHP から find
mroonga で全文検索する場合は、下記のような SQL を実行します。
SELECT * FROM diaries WHERE MATCH(content) AGAINST("hoge");
このようなのを CakePHP から実行します。
特にオーバーライドなど必要なく、これで検索できます。
<?php $this->Diary->find('all', array( 'conditions' => array('MATCH(content) AGAINST(?)' => array("hoge")) ));
SQL直ならこう。
<?php $this->Diary->getDatasource()->fetchAll( 'SELECT * FROM diaries WHERE MATCH(content) AGAINST(?)', array("hoge") );
こんな感じで、なんとかそれっぽく動くようになりました。
間違えてるとか、他に良い方法あれば、どなたか教えてください
m(_ _)m""
それから… mroonga のパーサを変更する場合、
インデックスのコメントにパーサを指定することが可能なんですがそれは対応してません。
もう一息なのは分かっているのですが力尽きて、my.cnf でデフォルト指定しました(笑)
誰か書いてください(ちらっちらっ>店長)
<追記>
あとこういうネタもありますが、需要があれば書きます
・mroonga インストール時、mysql 衝突事件。(CentOS5)
・cake testsuite 対応