Hatena::ブログ(Diary)

SH2の日記 RSSフィード

2009-12-06 MySQL InnoDBだけで全文検索 このエントリーを含むブックマーク

実験エントリです。

予習してみる

「転置インデックス」というキーワードで検索して、しばらく勉強してみます。

うーんなるほど。分かったような分からないような。

作ってみる

とりあえず、Twitter4Jを使ってこんなデータを用意しました。ちなみに人選は漢(オトコ)のコンピュータ道: MySQLerのTwitterアカウントまとめ。を参考にさせていただきました。

5707049458,2009-11-14 20:28:34,sakaik,@hbstudy  あ。そうですか〜。教えていただき
5707129844,2009-11-14 20:35:11,kazuho,InnoDB 1.0.5でバッファプールのGenerational
5707141572,2009-11-14 20:36:09,kazuho,RT @higepon: RT @yukichi: 「フリーランス」
5707146470,2009-11-14 20:36:34,_Fake,@_yuyu どうしてこうなった
5707159628,2009-11-14 20:37:41,hirose31,恵比寿でスーツコスなう
5707167760,2009-11-14 20:38:22,kazuho,old領域はデフォルトで37%。ロードしたデータ
5707190203,2009-11-14 20:40:17,stanaka,色々カスタマイズ可能な項目が増えるのは基
5707246432,2009-11-14 20:45:00,sh2nd,いきたかった #hbstudy
5707324541,2009-11-14 20:51:27,kazuho,mtstat は Monty Taylor's stat の略なのかな
5707385902,2009-11-14 20:56:23,_Fake,ブレーカー落ちた…

次に、タイムラインを格納するためのテーブルを作ります。

mysql> DESC timeline;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | bigint(20)   | NO   | PRI | NULL    |       |
| created_at  | datetime     | YES  |     | NULL    |       |
| screen_name | varchar(15)  | YES  |     | NULL    |       |
| text        | varchar(140) | YES  |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

もう一つテーブルを作ります。こういうのを転置インデックスと言うんだそうです。

mysql> DESC timeline_i;
+--------+------------+------+-----+---------+-------+
| Field  | Type       | Null | Key | Default | Extra |
+--------+------------+------+-----+---------+-------+
| bigram | varchar(2) | NO   | PRI |         |       |
| id     | bigint(20) | NO   | PRI | 0       |       |
+--------+------------+------+-----+---------+-------+

timelineテーブルにトリガを仕掛けます。

DROP TRIGGER IF EXISTS timeline_create_bigram;
DELIMITER //
CREATE TRIGGER timeline_create_bigram
AFTER INSERT ON timeline FOR EACH ROW
BEGIN
 DECLARE text_length INT;
 DECLARE text_index INT;
 
 SET text_length = CHAR_LENGTH(NEW.text);
 SET text_index = 1;
 
 WHILE text_index < text_length DO
  INSERT IGNORE INTO timeline_i (bigram, id)
         VALUES (SUBSTRING(NEW.text, text_index, 2), NEW.id);
  SET text_index = text_index + 1;
 END WHILE;
END;
//
DELIMITER ;

データロードを行います。

mysql> LOAD DATA LOCAL INFILE 'timeline.dat' INTO TABLE timeline;
Query OK, 35732 rows affected (1 min 34.76 sec)
Records: 35732  Deleted: 0  Skipped: 0  Warnings: 0

だいぶ遅いですけど、うまくロードできたみたいです。データロード後のテーブルは以下のようになります。

mysql> SELECT id, created_at, screen_name, LEFT(text, 20)
    -> FROM timeline ORDER BY id LIMIT 10 OFFSET 32000;
+------------+---------------------+-------------+---------------------------------------+
| id         | created_at          | screen_name | LEFT(text, 20)                        |
+------------+---------------------+-------------+---------------------------------------+
| 5706909826 | 2009-11-14 20:16:42 | sakaik      | 参加できなかったんだけど、ustないのん |
| 5706917932 | 2009-11-14 20:17:26 | kazuho      | オチを先に言うの禁止www #hbstu        |
| 5706918515 | 2009-11-14 20:17:30 | _Fake       | @_yuyu もうだまされないぞ!           |
| 5706994272 | 2009-11-14 20:23:52 | _Fake       | @_yuyu 日本語でお願いします!         |
| 5707049458 | 2009-11-14 20:28:34 | sakaik      | @hbstudy  あ。そうですか〜。教        |
| 5707129844 | 2009-11-14 20:35:11 | kazuho      | InnoDB 1.0.5でバッファプール          |
| 5707141572 | 2009-11-14 20:36:09 | kazuho      | RT @higepon: RT @yuk                  |
| 5707146470 | 2009-11-14 20:36:34 | _Fake       | @_yuyu どうしてこうなった             |
| 5707159628 | 2009-11-14 20:37:41 | hirose31    | 恵比寿でスーツコスなう                |
| 5707167760 | 2009-11-14 20:38:22 | kazuho      | old領域はデフォルトで37%。ロードし    |
+------------+---------------------+-------------+---------------------------------------+
10 rows in set (0.04 sec)

mysql> SELECT bigram, id FROM timeline_i
    -> ORDER BY bigram, id LIMIT 20 OFFSET 1280000;
+--------+------------+
| bigram | id         |
+--------+------------+
| 行性   | 2627699213 |
| 行情   | 4696663607 |
| 行手   | 6165165693 |
| 行振   | 1968230572 |
| 行支   | 5609167583 |
| 行支   | 5751891393 |
| 行政   | 4309254243 |
| 行政   | 5793749538 |
| 行数   |  814006785 |
| 行数   |  895299155 |
| 行数   | 3836569341 |
| 行数   | 3931668910 |
| 行数   | 4496147540 |
| 行方   |  773875969 |
| 行方   | 3121814754 |
| 行方   | 3976865730 |
| 行日   | 6055373012 |
| 行時   |  802293533 |
| 行時   | 3275845988 |
| 行時   | 3275991644 |
+--------+------------+
20 rows in set (0.75 sec)

検索してみる

普通にLIKE検索してみると、こんな感じです。

mysql> SELECT id, created_at, screen_name, text FROM timeline
    -> WHERE text LIKE '%味噌ラーメン%'\G
*************************** 1. row ***************************
         id: 5007144696
 created_at: 2009-10-20 10:46:49
screen_name: hirose31
       text: @sugizou 昼はねぎ味噌ラーメンにしまっす!
1 row in set (0.07 sec)

転置インデックスを組み合わせると、こうなります。

mysql> SELECT t.id, t.created_at, t.screen_name, t.text
    -> FROM timeline t INNER JOIN timeline_i i ON t.id = i.id
    -> WHERE i.bigram = '味噌'
    -> AND t.text LIKE '%味噌ラーメン%'\G
*************************** 1. row ***************************
         id: 5007144696
 created_at: 2009-10-20 10:46:49
screen_name: hirose31
       text: @sugizou 昼はねぎ味噌ラーメンにしまっす!
1 row in set (0.00 sec)

おー、速くなっているみたいですね!

測定してみる

適当に作ったわりには効果があるようなので、きちんと測定してみました。

今まではVMwareの環境を使っていたのですが、ベンチマークをするのに仮想化はないだろうということで古いPCを引っ張り出してきました。しばらくこれを使っていこうと思います。

いろいろな検索語で試してみたところ、以下のような結果になりました。

検索語ヒット件数転置インデックスLIKE検索
味噌ラーメン1件1msec85msec
チャーハン9件3msec85msec
PostgreSQL28件10msec88msec
チューニング32件2msec85msec
バックアップ70件6msec87msec
Oracle87件20msec89msec
Java117件9msec86msec
Drizzle176件5msec87msec
Perl208件10msec87msec
InnoDB225件31msec89msec
.ne.jp464件9msec89msec
MySQL979件19msec90msec
bit.ly1,396件27msec93msec
[space]@2,689件57msec105msec
RT3,028件61msec104msec
http://4,227件64msec103msec

このように、今回試した検索語においてはすべて転置インデックスを使った方が速いという結果が出ました。「http://」の場合は全体の12%もヒットしてしまうのですが、それでもLIKE検索でフルスキャンを行うよりは速いようです。グラフにすると傾向が見えてきます。

f:id:sh2:20091205234037p:image

もう少し工夫すれば、使いどころが出てくるかもしれませんね。

tanatana 2009/12/06 17:03 これって味噌と味噌ラーメンの複合っぽいですが、
やっぱり"味噌"の部分ってわかちされた後の単語ですよね。"味噌" "東京" "激安" と"味噌ラーメン" "東京芝浦激安" みたいな単語が複数個くっつくと、検索時に2gramしないとだめっすよね...

sh2sh2 2009/12/06 17:35 i.bigram = '味噌' のところは、
検索語から最初の2文字を持ってくるだけですよ。

timeline_i上では、潜在的に '味噌チャーシューメン' も
ヒットしていることになりますが、ある程度絞り込んだ上で最後に
t.text LIKE '%味噌ラーメン%' でフィルタリングするという方針です。

timeline_iの条件だけでidをぴったり特定しようとすると、
確かに難しそうですね。

なかにしなかにし 2009/12/06 22:21 おもしろいですね。自分も以前こんなことをやろうとしたことがあります。
最初の2文字だけじゃなく

SELECT t.id, t.created_at, t.screen_name, t.text
FROM timeline t
INNER JOIN timeline_i i1 ON t.id = i1.id
INNER JOIN timeline_i i2 ON t.id = i2.id
...
WHERE i1.bigram = '味噌' and i2.bigram = '噌ラ' and ...
AND t.text LIKE '%味噌ラーメン%'

とつなげていったほうが速いかもしれません。
自分の場合、この2文字でどれぐらい絞りこめるか統計データを取っておいて、その順にstraight_joinするとかしてました。
テキスト中の何文字目に表れるかというのを使えばLIKEを無くせますね。

トリガーを使うって発想は無かったなー。自分がやったときは、indexの構築にすごく時間がかかったんですが、トリガーを使ったら速いですか、そんなでもないですか?

sh2sh2 2009/12/07 00:11 なるほど、調べてみると確かに
'味噌' だと8件あるのですが '噌ラ' なら最初から1件に絞り込めます。

どのくらい絞り込めるのかは本来MySQLが統計情報として
持っているはずなので、それが使えるといいですね。

転置インデックスの構築は泣きたくなるほど遅いです。
最初はトリガではなく外部プログラムで転置インデックスを作っていたのですが、
あまり変わらない感じです。