全文検索(または単にテキスト検索)は、問い合わせを満たす自然言語の文書を識別し、更には問い合わせとの関連性の順に並び替えることができます。
もっとも一般的な検索は、与えられた検索語を含む文書を探し、問い合わせとの類似性の順に返す、というものです。
問い合わせ
と類似性
の記法は非常に柔軟で、特定の用途に適合できます。
もっとも単純な検索では、問い合わせ
は単語の集合として、類似性
は文書中の問い合わせ対象の単語の頻度として扱います。
テキスト検索演算子は、データベースシステムに長年存在していました。PostgreSQLは、テキストデータ型用に、~
,~*
, LIKE
,ILIKE
の各演算子を持っています。しかし、近代的な情報システムに必要な以下の本質的な特徴を欠いています。
英語にさえ、言語学的なサポートがありません。
派生語、たとえばsatisfies
に対してsatisfy
を容易に扱えないため、正規表現は十分ではありません。
satisfy
を探すときは、たぶんあなたはsatisfies
も探したいでしょうが、それらを含む文書は探せないかもしれません。
OR
を使えば複数の派生語を検索することができますが、退屈で間違いやすいです(ある種の単語は数千の派生語を持つことがあります)。
検索結果を順序付け(順位付け)することができません。その結果、数千の合致する文書が見つかったような場合に非効率的です。
インデックスをサポートしないので毎回検索時にすべての文書を処理しなければならず、遅いです。
全文検索のインデックス付けでは、文書を前もって処理しておき、後で素早く検索するために、インデックスを保存しておくことができます。前処理には以下があります。
文書からトークンを解析します。 トークンを色々なクラス、たとえば数、単語、複合単語、電子メールアドレスに分けて識別することが有効です。そうすれば、扱いを変えることができます。 原則として、トークンのクラスは、特定の用途に依存します。しかし、ほとんどの目的には、あらかじめ定義されたクラスの集合を使うのが適当です。 PostgreSQLは、パーサを使ってこの処理段階を実行します。 標準搭載のパーサが提供されますが、特別な用途にはカスタム仕様のパーサを作ることもできます。
トークンを語彙素(lexemes)に変換します。
語彙素はトークンと同じ文字列ですが、違う形態の同じ単語が同じになるように 正規化されています。
たとえば、正規化においてはほぼ常に大文字を小文字に変換し、(英語のs
またはes
のような)接尾辞を取り除くことが多いです。
これにより、可能性のあるすべての変種を地道に入力すること無く、同じ単語の変化形を検索できます。
また、このステップでは、あまりにありふれていて、検索の役に立たないストップワードを取り除くことが多いです。
(つまり、トークンは文書テキストの未加工の断片そのものであり、語彙素はインデックス付けや検索に有用と思われる単語です。)
PostgreSQLは、辞書を使ってこのステップを実行します。
いろいろな標準辞書が提供されています。特定の用途向けにカスタム辞書を作ることもできます。
検索に最適化された前処理済の文書を保存します。 たとえば、個々の文書は、正規化された語彙素の整列済の配列として表現されます。 語彙素とともに、適合性ランキング用に、位置情報を格納しておくことがしばしば望まれます。そうすることにより、問い合わせの語を「高密度」に含んでいる文書を、まばらに含む文書よりも高くランクづけすることができます。
辞書を使ってトークンの正規化を細かく制御できます。 適当な辞書を用意すれば次のようなことができます。
インデックスしたくないストップワードの定義
Ispellを使って、同義語を単一の単語に関連づける
類語辞書(thesaurus)を使って、成句を単一の単語に関連づける
Ispell辞書を使って、単語の変種を正規の単語に関連づける
Snowball語幹規則を使って、単語の変種を正規の単語に関連づける
前処理した文書を格納するために、データ型tsvector
が提供されています。また、処理済問い合わせを表現するためにtsquery
型も提供されています(8.11)。これらのデータ型のために、多数の関数と演算子が利用できますが(9.13)、もっとも重要なのは、12.1.2で紹介している@@
演算子です。全文検索はインデックス(12.9)を使って高速化できます。
文書は全文検索システムにおける検索の単位です。 たとえば、雑誌記事やメールのメッセージです。 テキスト検索エンジンは、文書をパースし、語彙素(キーワード)とそれが含まれる親文書の関連を格納できなければなりません。 後で、この関連を使って問い合わせ語を含む文書を検索するのに使います。
PostgreSQLでの検索においては、ドキュメントはデータベースのテーブルの行内のテキストフィールドか、あるいはそのようなフィールドの組み合わせ(結合)でもよいです。そうしたフィールドはおそらく複数のテーブルに格納されていたり、動的に獲得されるものであったりします。 言い換えると、文書はインデックス付けのために複数の異なる部分から構成されても良く、それらが全体としてはひとまとまりに格納されていなくても良いのです。例を示します。
SELECT title || ' ' || author || ' ' || abstract || ' ' || body AS document FROM messages WHERE mid = 12; SELECT m.title || ' ' || m.author || ' ' || m.abstract || ' ' || d.body AS document FROM messages m, docs d WHERE m.mid = d.did AND m.mid = 12;
実際には、これらの例の問い合わせでは、coalesce
を使って、一部NULLが含まれているためにドキュメント全体がNULLになってしまうのを防ぐべきです。
別な方法としては、ファイルシステム上に文書を単純なテキストファイルとして格納することです。この場合、データベースは、フルテキストインデックスを格納し、検索を実行するために使うことができます。ファイルシステムから文書を取り出すためには、何かのユニークな識別子を使います。しかし、データベースの外にあるファイルを取り出すには、スーパーユーザの許可か、特殊な関数のサポートが必要です。そういうわけでたいていの場合はPostgreSQLの中にすべてのデータを保持するのよりも不便です。また、すべてのデータをデータベースに保持することにより、文書のインデックス付けと表示の際に文書のメタデータにアクセスすることが容易になります。
テキスト検索という目的のため、各々の文書は前処理されてtsvector
形式に変換しておかなければなりません。検索と順位付けはすべてtsvector
表現の文書上で行われます。検索とランキングは文書のtsvector
表現上で実行されます — オリジナル文書は、ユーザに表示のため選択された場合にのみ取り出される必要があります。というわけで、ここではtsvector
を文書と見なすことがよくあります。といっても、tsvector
は完全な文書の縮小表現でしかありません。
PostgreSQLにおける全文検索は、tsvector
(文書)が、tsquery
(問い合わせ)に一致したら真を返す照合演算子@@
に基づいています。どちらのデータ型を先に書いても構いません。
SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat'::tsquery; ?column? ---------- t SELECT 'fat & cow'::tsquery @@ 'a fat cat sat on a mat and ate a fat rat'::tsvector; ?column? ---------- f
上記の例でわかるように、tsquery
は、tsvector
と違って、単なるテキストではありません。
tsquery
は正規化済の語彙素である検索表現を含み、AND, OR, NOT, FOLLOWED BY演算子を使って複数の表現を組み合わせても構いません。
(詳細は8.11.2を見てください。)
主にテキスト中の単語を正規化することにより、ユーザが入力したテキストを適切なtsquery
に変換するのに便利な関数to_tsquery
、plainto_tsquery
、phraseto_tsquery
があります。
同様に、文書文字列をパースして正規化するためにto_tsvector
が利用できます。
というわけで、実際にはテキスト検索照合はこんな感じになります。
SELECT to_tsvector('fat cats ate fat rats') @@ to_tsquery('fat & rat'); ?column? ---------- t
この照合は、もしつぎのように書くとうまくいかないことに注意してください。
SELECT 'fat cats ate fat rats'::tsvector @@ to_tsquery('fat & rat'); ?column? ---------- f
というのも、単語rats
に対して正規化が行われないからです。
tsvector
の要素は、すでに正規化されている語彙素であることになっているので、rats
はrat
に一致しません。
また、@@
演算子は、text
を入力として受付けるので、簡単に使うときには、明示的にテキスト文字列をtsvector
またはtsquery
に変換することを省略できます。応用として以下のものがあります。
tsvector @@ tsquery tsquery @@ tsvector text @@ tsquery text @@ text
最初の2つについてはすでに説明しました。
text
@@
tsquery
という形式は、to_tsvector(x) @@ y
と同じです。
text
@@
text
という形式は、to_tsvector(x) @@plainto_tsquery(y)
と同じです。
tsquery
内において、演算子 &
(AND) は、マッチと見なされるには引数の両方がドキュメント内に現れる必要があるということを指定します。
同様に、演算子 |
(OR) では、引数の少なくとも一方が現れる必要があり、また演算子 !
(NOT) は、マッチと見なされるには引数が現れてはならないことを指定します。
例えば、fat & ! rat
という問い合わせは、fat
は含むがrat
は含まないドキュメントとマッチします。
句の検索は、tsquery
演算子 <->
(FOLLOWED BY)を使うことで可能です。
この演算子は、その引数にマッチする語が隣接していて、かつ指定と同じ順序である場合にのみマッチします。
例を示します。
SELECT to_tsvector('fatal error') @@ to_tsquery('fatal <-> error'); ?column? ---------- t SELECT to_tsvector('error is not fatal') @@ to_tsquery('fatal <-> error'); ?column? ---------- f
FOLLOWED BY演算子にはもっと汎用的なバージョンがあり、それは<
という構文で使います。
ここでN
>N
は整数で、マッチする語彙素の位置の差を表します。
<1>
は<->
と同じですが、<2>
ではマッチする語の間にちょうど1つ、他の語彙素が現れることを許容する、という具合です。
phraseto_tsquery
関数は、この演算子を利用して、ストップワードを含む複数語の句にマッチ可能なtsquery
を構築するものです。
例を示します。
SELECT phraseto_tsquery('cats ate rats'); phraseto_tsquery ------------------------------- 'cat' <-> 'ate' <-> 'rat' SELECT phraseto_tsquery('the cats ate the rats'); phraseto_tsquery ------------------------------- 'cat' <-> 'ate' <2> 'rat'
ときに役立つことがある特別な場合として、<0>
を2つのパターンが同じ語にマッチすることを要求するために使うことができます。
tsquery
演算子を入れ子にして管理するために括弧を使うことができます。
括弧がない場合、|
の結合が最も弱く、次が&
、その次が<->
で、!
が最も強く結合します。
FOLLOWED BYの中ではマッチの正確な位置が重要ですので、AND/OR/NOT演算子は、FOLLOWED BY演算子の引数の中で使われる場合にはそうでない場合と微妙に異なる意味になることに言及しておく価値はあります。
例えば、通常!x
はx
をどこにも含まない文書とのみマッチします。
しかし、!x <-> y
は、x
の直後にあるのでなければy
とマッチします。文書の他のところでのx
の出現は、マッチを邪魔しません。
もう一つの例は、x & y
は通常x
とy
の両方が文書のどこかに現れることだけを要求しますが、(x & y) <-> z
は、x
とy
が同じ場所、z
の直前でマッチすることを要求します。
そのため、この問い合わせはx <-> z & y <-> z
とは異なった振る舞いをします。後者は2つの別の文字列、x z
とy z
を含む文書にマッチします。
(x
とy
が同じ場所でマッチすることはあり得ませんので、上に書いたこの特別な問い合わせは、役に立ちません。しかし、接頭辞マッチパターンのようにより複雑な状況では、この形の問い合わせは役に立つかもしれません。)
今までのはすべて単純なテキスト検索の例でした。
すでに述べたように、全文検索機能を使えば、もっと色々なことができます。
インデックス付けの際に特定の単語をスキップ(ストップワード)、同義語(synonym)処理、賢いパース処理、すなわち、単に空白区切りに基づくパース処理以上のものです。
この機能はテキスト検索設定で制御します。
PostgreSQLには、多くの言語用の設定があらかじめ組み込まれていますが、ユーザ設定を容易に作ることもできます。
(psqlの\dF
コマンドで、利用できる設定を表示できます。)
インストールの際には、適当な設定が選ばれ、default_text_search_configがpostgresql.conf
中にセットされます。クラスタ全体で同じ設定を使用する場合はpostgresql.conf
の設定値を利用できます。クラスタの設定とは異なるが、あるデータベースの中で同じ設定を使う場合には、ALTER DATABASE ... SET
を利用します。さもなければ、セッション単位でdefault_text_search_config
を設定できます。
設定に依存するテキスト検索関数は、オプションでregconfig
引数を持っており、使用する設定を明示的に指定できます。default_text_search_config
は、この引数が省略されたときだけ使用されます。
カスタムテキスト検索設定を作り易くするため、設定はより単純なデータベースオブジェクトから作られます。PostgreSQLのテキスト検索機能は、4つの設定関連のデータベースオブジェクトを提供しています。
テキスト検索パーサは、文書をトークンに分解し、トークンを分類します(たとえば、単語とか数のように)。
テキスト検索辞書はトークンを正規化された形式に変換し、ストップワードを排除します。
テキスト検索テンプレートは、現在の辞書が利用する関数を提供します(辞書は、単にテンプレートと、その引数の集合を指定するだけです)。
テキスト検索設定は、パーサと使用する辞書の集合を選択し、パーサが生成したトークンを正規化します。
テキスト検索パーサとテンプレートは、低レベルのC関数で作ります。したがって、新しく開発するためにはCのプログラミング能力と、データベースにインストールするためのスーパーユーザ権限が必要になります。(PostgreSQLの配布物のcontrib/
には、追加パーサとテンプレートの例があります)。辞書と設定は、単に配下のパーサとテンプレートのパラメータを設定し、両者を結び付けるだけなので、新しい辞書と設定を作るために特別な権限は必要ありません。この章の後でカスタム辞書と設定を作る例が登場します。