データベースを使用するからには目的のデータを高速に検索したいと誰もが考えるでしょう。
高速な検索を実現する最も簡単な方法は適切な列 (例えば、検索条件に指定されている列) にインデックスを作成することです。 インデックス (索引) とは、その名のとおり書籍の索引と同じく目的のページ (データ) をすばやく探すための仕組みです。 もし、書籍に索引がなければ目的のページを探すのに最初から最後まで 1 ページずつ読んで探していく必要があります。
しかし、通常のインデックス (B-Tree インデックス) は「WHERE c = ‘word’ (列 c の値が word という文字列と一致するか)」といった完全一致検索や、「WHERE c LIKE ‘word%’ (列 c の値の先頭に word という文字列を含まれているか)」といった前方一致検索でしか使えません。 そのため、「WHERE c LIKE ‘%word%’ (列 c の値に word という文字列が含まれているか)」といった部分一致検索では、インデックスを使えずにすべての行を調べるため非常に時間がかかってしまいます。
今回は、そのような部分一致検索を高速化する全文検索を PowerGres および PostgreSQL で実現する方法について紹介します。
全文検索とは、複数の文書にまたがるテキストから特定の文字列を検索することです。 一般的に全文検索の機能を持つといった場合には、ただ検索できるだけではなく、高速に検索できることを意味します。 PostgreSQL では、バージョン 8.3 で tsearch5 と呼ばれる全文検索のモジュールが本体に取り込まれ、PostgreSQL 単体で高速な全文検索を実現できるようになっています。
PostgreSQL では、テキストから検索対象となる文字列を抽出し、文字列とその位置情報のリストからから構成される特別なインデックス (GIN インデックス) を作成し、インデックスを使用して高速な全文検索を実現しています。
その際、英語のように単語が空白で区切られていればいいのですが、日本語の場合には何らかの方法でテキストから検索対象となる文字列を抽出する必要があります。 日本語のテキストから検索対象となる文字列を抽出するには、大きく分けて形態素解析 (わかち書き) 方式と N-Gram 方式という 2 つの方法があります。
形態素解析方式は、テキストを形態素と呼ばれる言語において意味を持つ最小単位 (単語のようなもの) に分解して文字列を抽出する方式です。 単語を空白で区切って書くことをわかち書きと呼ぶことからわかち書き方式とも呼ばれます。 日本語のように単語を空白で区切って書かない場合には、形態素への分解には形態素解析エンジン (KAKASI や MeCab、ChaSenなど) が必要となります。
テキストの意味を考慮して文字列を抽出するので、「京都」で検索して「東京都」がヒットするといった検索ノイズは少ないですが、その反面、形態素に一致しなければヒットしないため検索漏れが多いという特徴があります。
N-Gram 方式は、テキストを任意の文字数で区切って文字列を抽出する方式です。 2 文字ずつ区切った場合には Bi-Gram (2-Gram)、3 文字ずつ区切った場合には Tri-Gram (3-Gram) と呼びます。
テキストの意味を考慮せずに機械的に文字列を抽出するので、形態素解析方式とは逆に検索漏れが少なく検索ノイズが多いという特徴があります。 また、形態素解析方式と比べてインデックスのサイズが大きくなりやすいという特徴があります。
PostgreSQL 本体に全文検索の機能があるといっても、日本語の全文検索を行うにはテキストから文字列を抽出する必要があります。 今回は、PowerGres に付属する MeCab と textsearch_ja を使用して形態素解析方式で日本語の全文検索を行います。
MeCab はテキストを形態素に分解する形態素解析エンジンの 1 つです。 textsearch_ja は PostgreSQL から MeCab を使用して全文検索を行うためのモジュールです。 いずれもオープンソースのソフトウェアとして以下の URL から配布されています。
PowerGres の場合には、MeCab と textsearch_ja がいっしょにインストールされるので、別途インストールすることなく、簡単な設定だけで全文検索の機能を使用できます。 PowerGres のインストールがまだ完了していない方は、「第 1 回 PowerGres を使ってみよう」を読んでインストールしてください。
PostgreSQL の場合には、上記の URL からファイルをダウンロードして MeCab と textsearch_ja をインストールしてください。
次に textsearch_ja の関数をデータベースに登録して PostgreSQL から MeCab を使用できるようにします。 textsearch_ja の関数を登録するには psql でデータベースにファイル textsearch_ja.sql を読み込みます。
textsearch_ja.sql は PowerGres では以下のパスにインストールされています。
(PowerGres のインストール先)\share\contrib\textsearch_ja.sql
PowerGres Manager から psql を起動した場合には、textsearch_ja.sql を読み込む前に \c コマンドで userdb データベースに接続し、SET ROLE 文で一般ユーザ testuser に切り替わります。 その後、\! コマンドで外部コマンドとして psql を実行して textsearch_ja.sql を読み込むのが簡単です。 また、\encoding コマンドでクライアントの文字エンコーディングを SJIS に変更しておきます。
psql (8.4.3) Type "help" for help. postgres=# \c userdb psql (8.4.3) You are now connected to database "userdb". userdb=# SET ROLE testuser; SET userdb=> \! psql -f ..\share\contrib\textsearch_ja.sql -d userdb SET BEGIN CREATE FUNCTION CREATE FUNCTION CREATE FUNCTION CREATE TEXT SEARCH PARSER COMMENT CREATE FUNCTION CREATE TEXT SEARCH TEMPLATE CREATE TEXT SEARCH DICTIONARY CREATE TEXT SEARCH CONFIGURATION COMMENT ALTER TEXT SEARCH CONFIGURATION ALTER TEXT SEARCH CONFIGURATION ALTER TEXT SEARCH CONFIGURATION CREATE FUNCTION CREATE FUNCTION CREATE FUNCTION CREATE FUNCTION CREATE FUNCTION CREATE FUNCTION CREATE FUNCTION COMMIT userdb=> \encoding SJIS
textsearch_ja が正しく動作することを確認するため、textsearch_ja の ja_wakachi 関数でテキストのわかち書き (形態素への分解) を行ってみましょう。
userdb=> SELECT ja_wakachi('すもももももももものうち'); ja_wakachi -------------------------------- すもも も もも も もも の うち (1 row)
上記のように、「すもももももももものうち」というテキストのわかち書きが行われ、「すもも」、「も」、「もも」、「も」、「もも」、「の」、「うち」という空白で区切られた文字列が出力されたでしょうか?
以下のようなエラーメッセージが出力された場合には、textsearch_ja の関数が正しく登録できていない可能性があります。 データベースに textsearch_ja.sql を読み込む際にエラーが発生していなかったかを確認してください。
ERROR: function ja_wakachi(unknown) does not exist LINE 1: SELECT ja_wakachi('すもももももももものうち'); ^ HINT: No function matches the given name and argument types. You might need to add explicit type casts.
全文検索の機能を確認するため、テーブルにデータを投入して全文検索を行ってみましょう。 今回は、日本 PostgreSQL ユーザ会が公開している PostgreSQL 日本語マニュアル (HTML 形式) を使って、指定した単語の含まれるファイルを検索してみたいと思います。
テーブルの作成とデータの投入を行う SQL の書かれたファイルを用意してあります。 以下の URL から ZIP 形式のアーカイブをダウンロードして適当なフォルダに展開してください。
textsearch-sample.zip
アーカイブを展開すると textsearch-sample.sql というファイルが抽出されます。 次に psql でデータベースに textsearch-sample.sql を読み込みます。
userdb=> \! psql -f (アーカイブの展開先)\textsearch-sample.sql userdb SET SET SET SET SET SET SET SET SET CREATE TABLE ALTER TABLE
textsearch-sample.sql を読み込むと manual という名前のテーブルが作成されます。 \d コマンドで manual テーブルのテーブル定義を確認してみましょう。
userdb=> \d manual Table "public.manual" Column | Type | Modifiers ----------+------+----------- filename | text | not null content | text | not null Indexes: "manual_pkey" PRIMARY KEY, btree (filename)
manual テーブルはマニュアルのデータが格納されるテーブルです。 filename 列にはマニュアルのファイル名、content 列にはファイルの内容が格納されています。 マニュアルは 991 個のファイルから構成されているので、manual テーブルの行数は 991 行となります。
全文検索を高速化するにはインデックスを作成する必要があります。
インデックスの作成時に種類を指定しなかった場合には、B-Tree と呼ばれる種類のインデックスが作成されます。 B-Tree インデックスはツリー構造となっており、値の大小関係によってノードをたどって目的のデータをすばやく探すことができます。
完全一致検索や前方一致検索の場合には、値の大小関係を比べて目的のデータを検索できるので、B-Tree インデックスを使って検索を高速化できます。 しかし、部分一致検索の場合には、データに含まれる文字列を検索する必要があるので、値全体の大小関係では目的のデータを検索できません。 従って、すべてのデータを調べて文字列が含まれているかをチェックしなければならずに時間がかかってしまいます。
それでは、部分一致検索のような全文検索を高速化するにはどうしたらいいのでしょうか?
PostgreSQL では、バージョン 8.1 で GIN (Generalized Inverted Index; 汎用転置インデックス) と呼ばれるインデックスが本体に組み込まれました。 GIN インデックスは、キーとなる値とその位置情報のリストから構成されるデータ構造を格納するインデックスです。 全文検索を高速化するにはこの GIN インデックスを使用します。
テキストから抽出された文字列とその位置情報を格納するための tsvector と呼ばれるデータ型が用意されており、GIN インデックスはテキストから変換された tsvector 型のデータに対して作成します。 テキストを tsvector 型に変換するには to_tsvector 関数を使用します。
to_tsvector 関数にテキストを渡して tsvector 型に変換してみましょう。
userdb=> SELECT to_tsvector('A fat cat sat on a mat and ate a fat rat.'); to_tsvector ----------------------------------------------------- 'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4 (1 row)
to_tsvector 関数による tsvector 型への変換はテキスト検索の設定に従って行われます。 テキスト検索の設定は to_tsvector 関数の第 1 引数として指定できます。 その場合には、テキストは第 2 引数として渡します。
userdb=> SELECT to_tsvector('english', 'A fat cat sat on a mat and ate a fat rat.'); to_tsvector ----------------------------------------------------- 'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4 (1 row)
指定できるテキスト検索の設定は \dF コマンドで確認できます。 textsearch_ja をインストールするとテキスト検索の設定として「japanese」が使用できるようになります。
userdb=> \dF List of text search configurations Schema | Name | Description ------------+------------+--------------------------------------- pg_catalog | danish | configuration for danish language pg_catalog | dutch | configuration for dutch language pg_catalog | english | configuration for english language pg_catalog | finnish | configuration for finnish language pg_catalog | french | configuration for french language pg_catalog | german | configuration for german language pg_catalog | hungarian | configuration for hungarian language pg_catalog | italian | configuration for italian language pg_catalog | japanese | configuration for japanese language pg_catalog | norwegian | configuration for norwegian language pg_catalog | portuguese | configuration for portuguese language pg_catalog | romanian | configuration for romanian language pg_catalog | russian | configuration for russian language pg_catalog | simple | simple configuration pg_catalog | spanish | configuration for spanish language pg_catalog | swedish | configuration for swedish language pg_catalog | turkish | configuration for turkish language (17 rows)
テキスト検索の設定を指定せずに to_tsvector 関数を実行した場合には、default_text_search_config パラメータの設定に従います。 default_text_search_config パラメータのデフォルト値はデータベースクラスタ作成時のロケールの指定に依存します。 PowerGres の場合には、ロケールなし (C ロケール) を指定してデータベースクラスタを作成しているので、default_text_search_config パラメータの値は「english」になっているはずです。
userdb=> SHOW default_text_search_config; default_text_search_config ---------------------------- pg_catalog.english (1 row) userdb=> SELECT to_tsvector('マットに座った太ったネコが太ったネズミを食べました。'); to_tsvector ---------------------------------------------------------- 'マットに座った太ったネコが太ったネズミを食べました。':1 (1 row)
テキスト検索の設定を指定しなくても日本語の全文検索を行えるようにするため、default_text_search_config パラメータの値を「japanese」に変更しておきます。
userdb=> SET default_text_search_config TO japanese; SET userdb=> SELECT to_tsvector('マットに座った太ったネコが太ったネズミを食べました。'); to_tsvector --------------------------------------------------------------- 'ネコ':4 'ネズミ':6 'マット':1 '太る':3,5 '座る':2 '食べる':7 (1 row)
tsvector 型への変換結果を見ると分かりますが、変換前のテキストに含まれていた単語が変換後にはなくなっていたり (例えば、「マットに」の「に」がなくなっています)、動詞が変換後には終止形になっていたりします (例えば、「座った」が「座る」になっています)。 これは、to_tsvector 関数による tsvector 型への変換時に単語が正規化され、ストップワードが取り除かれたためです。
正規化とは、テキストに含まれる単語と完全に一致しなくても検索できるように、文字の種類を変換したり、単語の語形を変換したりする処理のことです。 また、ストップワードとは、日本語では「てにをは (助詞)」のようにほとんどのテキストに含まれており、一般的にインデックスを作成する価値のない単語のことです。
最後に manual テーブルの content 列に GIN インデックスを作成し、インデックスを使用して高速な全文検索が行えるようにしておきましょう。
userdb=> CREATE INDEX manual_content_idx userdb-> ON manual USING gin (to_tsvector('japanese', content)); CREATE INDEX userdb=> ANALYZE manual; ANALYZE
CREATE INDEX 文では、GIN インデックスを作成するために「USING gin」を指定しています。 また、tsvector 型のデータに対してインデックスを作成するため、to_tsvector 関数の実行結果に対して式インデックスを作成しています (式インデックス方式)。 インデックスの作成方式には、式インデックス方式以外にも tsvector 型のデータを格納する列を追加してその列に対してインデックスを作成する方式もあります (別列方式)。
式インデックス方式は、別列方式と比べて設定が簡単でディスク容量をあまり消費しなくて済みますが、検索のたびに to_tsvector 関数を実行するので遅くなるという特徴があります。 インデックスの作成方式について詳しくは PostgreSQL 日本語マニュアルの「12.2.2. インデックスの作成」を参照してください。
なお、式インデックスでは、テーブル内のデータが変更されることなく、同じ引数が与えられた場合には、式の実行結果が常に同じである必要があります。 to_tsvector 関数は引数としてテキスト検索の設定を省略することもできますが、それでは default_text_search_config パラメータの設定に依存して実行結果が常に同じとはなりません。 従って、インデックスの作成時には、テキスト検索の設定として「japanese」を明示的に指定しています。
ちなみにインデックスの作成後に実行している ANALYZE 文は統計情報を更新する SQL です。 統計情報とは、SQL の実行時に最も効率のいい実行方法を決定するのに使われる情報です。 統計情報が実際のデータ状況とかけ離れていると、SQL を効率よく実行できずに時間がかかってしまうことがあります。 ANALYZE 文は、自動バキュームによって自動的に実行されるとは言え、大量のデータを投入した後やインデックスを作成した後には手動で実行しておくのがいいでしょう。
全文検索の検索条件を指定するには、通常の検索に用いる = や LIKE などの演算子は使用せず、tsquery 型の値として検索条件を書いて @@ 演算子を使用します。 とは言っても、通常の検索と比べてそれほど難しいものではありません。
検索条件を表す tsquery 型への変換には to_tsquery 関数を使用します。 例えば、manual テーブルから「全文検索」という単語を含むファイルを検索するには以下のように SQL を実行します。
userdb=> SELECT filename FROM manual userdb-> WHERE to_tsquery('全文検索') @@ to_tsvector('japanese', content); filename ------------------------------- bookindex.html datatype-textsearch.html datatype-xml.html (省略) textsearch.html (32 rows)
もし、とくにエラーメッセージが出力されずに検索結果が 0 行の場合には、テキスト検索の設定が「japanese」となっていない可能性があります。 パラメータ default_text_search_config の値が「japanese」になっているかを確認してください。
検索条件には、1 つの単語だけではなく演算子を使って複数の単語を指定することもできます。 検索条件に指定できる演算子とその使用例は以下のとおりです。
演算子 | 説明 | 使用例 | 使用例の意味 |
---|---|---|---|
& | 論理積 | 太る & ネズミ | 「太る」と「ネズミ」を含む |
| | 論理和 | 太る & (ネズミ | ネコ) | 「太る」を含み、「ネズミ」または「ネコ」を含む |
! | 否定 | 太る & ネズミ & ! ネコ | 「太る」と「ネズミ」を含み、「ネコ」を含まない |
to_tsquery 関数を使うと複雑な検索条件を指定できますが、その反面、検索条件の書式に従っていないとエラーとなってしまいます。
userdb=> SELECT to_tsquery('太る & (ネズミ | ネコ'); ERROR: syntax error in tsquery: "太る & (ネズミ | ネコ"
指定した単語をすべて含むという単純な検索条件を指定できるだけでよければ、plainto_tsquery 関数を使うのが便利です。 plainto_tsquery 関数では、引数として渡された文字列を単語に区切って正規化を行い、それらの単語を & 演算子で結合して tsquery 型の値に変換してくれます。
userdb=> SELECT plainto_tsquery('マットに座った太ったネコが太ったネズミを食べました。'); plainto_tsquery -------------------------------------------------------------------- 'マット' & '座る' & '太る' & 'ネコ' & '太る' & 'ネズミ' & '食べる' (1 row)
最後にインデックスを使うことで検索性能がどの程度向上するかということを確認してみましょう。
まず、\timing コマンドを実行し、SQL の実行時間が表示されるようにします。 次に、インデックスの使えない LIKE 演算子による部分一致検索と、インデックスの使える @@ 演算子による全文検索をそれぞれ行ってみます。
userdb=> \timing Timing is on. userdb=> SELECT filename FROM manual WHERE content LIKE '%全文検索%'; filename ------------------------------- bookindex.html datatype-textsearch.html datatype-xml.html (省略) textsearch.html (32 rows) Time: 197.468 ms userdb=> SELECT filename FROM manual userdb-> WHERE to_tsquery('全文検索') @@ to_tsvector('japanese', content); filename ------------------------------- bookindex.html datatype-textsearch.html datatype-xml.html (省略) textsearch.html (32 rows) Time: 0.749 ms
上記の結果を見ればすぐにお分かりいただけるように、SQL の実行時間はインデックスの使えない場合が 197.468 ミリ秒であるのに対して使える場合には 0.749 ミリ秒、100 倍以上の高速化を実現できています。
今回は、PowerGres および PostgreSQL で高速な全文検索を実現する方法について紹介しました。 インデックスを使った全文検索がいかに高速であるかということをご理解いただけたでしょうか。
MeCab と textsearch_ja を使った形態素解析方式の全文検索は、本文でも説明しているように検索ノイズが少ない反面検索漏れが多いという特徴があります。 とは言え、システムの要件によっては検索漏れがあっては困るということもあるでしょう。 textsearch_ja を開発しているtextsearch-ja プロジェクトでは、textsearch_ja 以外にも N-gram 方式の全文検索モジュールとして、全文検索エンジン Senna を使った textsearch_senna と、全文検索エンジン Groonga を使った textsearch_groonga を開発しています。 N-gram 方式の全文検索を行いたい場合には、これらの全文検索モジュールを使ってみるのがいいでしょう。
次回は、「ボトルネックとなっている SQL を見付け出して性能を改善しよう」と題して、実行時間のかかっている SQL の特定方法と EXPLAIN 文による SQL の改善方法について紹介します。