Ludia / 全文検索の動作確認
インストールが終わったので、sampleデータベースを作成します。
CREATE DATABASE sample ENCODING = 'EUC_JP';
sampleデータベースでsennaが利用できるようにソースファイル内にあるpgsenna2.sqlを実行します。
$ psql -f ./pgsenna2.sql sample
次にサンプルのダミーデータを投入して、動作確認をします。手っ取り早く、日本郵政公社が提供している「郵便番号データ」を使うことにします。ダウンロードは以下からできます。
動作確認用のテーブルを作成し、データを投入します。
CREATE TABLE m_address ( id integer , zip_code_5 char(5) , zip_code char(7) , ken_kana text , city_kana text , machi_kana text , ken text , city text , machi text , machi_flag integer , aza_flag integer , chou_flag integer , two_cho_flag integer , update_flag integer , update_reason integer ); \copy m_address from '/path/to/csv/ken_all.euc.csv' with csv
インデックスを作成します。とりあえず分かち書き方式でインデックスを作成してあります。
CREATE INDEX idx_m_address_city_senna ON m_address USING fulltext(city);
投入したデータ件数をあらかじめ調べておきます。約12万件といった感じです。
SELECT count(*) FROM m_address ; count -------- 121785 (1 row)
では実際のクエリーを発行してみます。まずは単純な部分一致検索を行ないました。
SELECT id, zip_code, ken, city, machi FROM m_address WHERE city @@ '山'; id | zip_code | ken | city | machi -------+----------+----------+------------------+---------------------- 26110 | 6078416 | 京都府 | 京都市山科区 | 御陵進藤町 26110 | 6078419 | 京都府 | 京都市山科区 | 御陵檀ノ後 (中略) 43208 | 8610406 | 熊本県 | 山鹿市 | 菊鹿町下内田 43208 | 8610405 | 熊本県 | 山鹿市 | 菊鹿町下永野 26110 | 6078412 | 京都府 | 京都市山科区 | 御陵四丁野町 (1791 rows)
EXPLAIN ANALYZEで実行内容を見てみると、以下の通り。ばっちりインデックスが使われています。
EXPLAIN ANALYZE SELECT id, zip_code, ken, city, machi FROM m_address WHERE city @@ '山'; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------- Index Scan using idx_m_address_senna on m_address (cost=0.00..0.01 rows=122 width=48) (actual time=0.021..4.258 rows=1791 loops=1) Index Cond: (city @@ '山'::text) Total runtime: 7.590 ms (3 rows)
ちなみに通常のLIKE句での検索の場合だと以下の通り。当たり前のごとくインデックスは使われないのでかなり遅いです。
EXPLAIN ANALYZE SELECT id, zip_code, ken, city, machi FROM m_address WHERE city LIKE '%山%'; QUERY PLAN ---------------------------------------------------------------------------------------------------------------- Seq Scan on m_address (cost=0.00..4169.31 rows=24357 width=48) (actual time=4.619..143.090 rows=8223 loops=1) Filter: (city ~~ '%山%'::text) Total runtime: 151.494 ms (3 rows)
次にAND検索を行なってみました。sennaのクエリーの書式によると「*D+ 」とすればAND検索になります。
SELECT id, zip_code, ken, city, machi FROM m_address WHERE city @@ '*D+ 京都 山'; id | zip_code | ken | city | machi -------+----------+--------+--------------+---------------------- 26110 | 6070000 | 京都府 | 京都市山科区 | 以下に掲載がない場合 26110 | 6078423 | 京都府 | 京都市山科区 | 御陵山ノ谷 (中略) 26110 | 6078083 | 京都府 | 京都市山科区 | 竹鼻木ノ本町 26110 | 6078087 | 京都府 | 京都市山科区 | 竹鼻サイカシ町 26110 | 6078003 | 京都府 | 京都市山科区 | 安朱稲荷山町 (290 rows)
EXPLAIN ANALYZEで実行内容を見てみると、以下の通り。ばっちりインデックスが使われています。AND検索の場合だと、単一キーワードと実行速度は若干速くなっています。(誤差の範囲内かな?)
EXPLAIN ANALYZE SELECT id, zip_code, ken, city, machi FROM m_address WHERE city @@ '*D+ 京都 山'; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------ Index Scan using idx_m_address_senna on m_address (cost=0.00..0.01 rows=122 width=48) (actual time=0.027..0.694 rows=290 loops=1) Index Cond: (city @@ '*D+ 京都 山'::text) Total runtime: 5.724 ms (3 rows)
ここでも通常のLIKE句での検索の場合だと以下の通り。こちらもインデックスを使わない上にINTERSECT句を使っているので遅いですね。
EXPLAIN ANALYZE SELECT id, zip_code, ken, city, machi FROM m_address WHERE city LIKE '%京都%' INTERSECT SELECT id, zip_code, ken, city, machi FROM m_address WHERE city LIKE '%山%'; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------------- SetOp Intersect (cost=10444.72..10824.69 rows=2533 width=48) (actual time=426.147..445.195 rows=504 loops=1) -> Sort (cost=10444.72..10508.05 rows=25331 width=48) (actual time=408.068..421.484 rows=13219 loops=1) Sort Key: id, zip_code, ken, city, machi -> Append (cost=0.00..8591.93 rows=25331 width=48) (actual time=80.701..341.558 rows=13219 loops=1) -> Subquery Scan "*SELECT* 1" (cost=0.00..4179.05 rows=974 width=48) (actual time=80.698..149.655 rows=4996 loops=1) -> Seq Scan on m_address (cost=0.00..4169.31 rows=974 width=48) (actual time=80.690..137.502 rows=4996 loops=1) Filter: (city ~~ '%京都%'::text) -> Subquery Scan "*SELECT* 2" (cost=0.00..4412.88 rows=24357 width=48) (actual time=4.555..165.963 rows=8223 loops=1) -> Seq Scan on m_address (cost=0.00..4169.31 rows=24357 width=48) (actual time=4.546..146.403 rows=8223 loops=1) Filter: (city ~~ '%山%'::text) Total runtime: 449.568 ms (11 rows)
最後にOR検索も試してみました。sennaのクエリーの書式によると「*DOR 」とすればOR検索になります。
SELECT id, zip_code, ken, city, machi FROM m_address WHERE city @@ '*DOR 京都 港区'; id | zip_code | ken | city | machi -------+----------+--------+----------------+-------------------------------------------------------------------------- 40625 | 8240113 | 福岡県 | 京都郡みやこ町 | 吉岡 13103 | 1066154 | 東京都 | 港区 | 六本木六本木ヒルズ森タワー(54階) (中略) 26109 | 6011452 | 京都府 | 京都市伏見区 | 小栗栖西ノ峯 26109 | 6011451 | 京都府 | 京都市伏見区 | 小栗栖鉢伏 40625 | 8240115 | 福岡県 | 京都郡みやこ町 | 光冨 (5786 rows)
EXPLAIN ANALYZEで実行内容を見てみると、以下の通り。ばっちりインデックスが使われています。OR検索なので、やはり遅くなります。
EXPLAIN ANALYZE SELECT id, zip_code, ken, city, machi FROM m_address WHERE city @@ '*DOR 京都 港区'; QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------- Index Scan using idx_m_address_senna on m_address (cost=0.00..0.01 rows=122 width=48) (actual time=0.028..13.910 rows=5786 loops=1) Index Cond: (city @@ '*DOR 京都 港区'::text) Total runtime: 23.376 ms (3 rows)
LIKE句での検索の場合だと以下の通り。こちらもインデックスを使わない上にUNION句を使っているので遅いですね。
EXPLAIN ANALYZE SELECT id, zip_code, ken, city, machi FROM m_address WHERE city LIKE '%京都%' UNION SELECT id, zip_code, ken, city, machi FROM m_address WHERE city LIKE '%港区%'; QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------- Unique (cost=8464.54..8493.76 rows=1948 width=48) (actual time=305.030..326.259 rows=5786 loops=1) -> Sort (cost=8464.54..8469.41 rows=1948 width=48) (actual time=305.022..310.922 rows=5786 loops=1) Sort Key: id, zip_code, ken, city, machi -> Append (cost=0.00..8358.10 rows=1948 width=48) (actual time=77.272..276.977 rows=5786 loops=1) -> Seq Scan on m_address (cost=0.00..4169.31 rows=974 width=48) (actual time=77.267..133.454 rows=4996 loops=1) Filter: (city ~~ '%京都%'::text) -> Seq Scan on m_address (cost=0.00..4169.31 rows=974 width=48) (actual time=39.233..132.089 rows=790 loops=1) Filter: (city ~~ '%港区%'::text) Total runtime: 333.575 ms (9 rows)
たしかに速度は劇的に改善していますね。ちなみにLIKE句を使った場合に検索結果のレコード数を確認したところ、sennaクエリーでの検索結果のレコード数と異なっていました。多分、ここら辺の話なのかなと思います。(調べていません)
検索結果の数が数値1よりも小さい場合、完全一致→非わかち書き→部分一致の順に自動的に検索処理方法を切り替えます。完全一致でヒットした文書と比べて非わかち書き一致、部分一致でヒットした文書には数値2分だけ小さいスコアを付与します。数値2を省略した場合は既定値(=2)と解釈されます。
ちなみに上記とは別に大量データ(約440万件)を投入して、検索を行なった場合、LIKE句による部分一致検索に比べて、sennaクエリーを利用した全文検索の方が、劇的に速くなっていました。参考までに数値を載せておきます。
sennaクエリー(形態素解析) | Total runtime: 108.719 ms | ||
sennaクエリー(2-gram) | Total runtime: 139.390 ms | ||
LIKE句 | Total runtime: 10680.594 ms |
あとは以下の点については詳しく調べたほうがよいですね。
- インデックスデータがどの程度のディスク容量を必要とするのか?
- インデックス作成時にかかる時間
ちなみにいくつか制約事項があるのでREADMEファイルには目を通しておいたほうがよいです。
- 複数列インデックスとしては使用できません。
- 一意性インデックスの機能は提供しません。
- VACUUMには完全に対応していません。
無効なTIDのチェックは行われますが、インデックスのサイズは減少しません。
- REINDEXには対応していません。
インデックスの再構築は、
インデックスファイルの削除、インデックスのDROP、インデックスの再作成、
という手順で行ってください。