Ludia 1.5.2 README

目次

Ludiaについて

概要

LudiaはPostgreSQLに高速な全文検索機能を提供します。 全文検索エンジンSennaを利用し、データベース内のテキスト情報を高速検索します。 Ludiaは以下のような特徴をもっています。

PostgreSQLインデックス機能への統合
PostgreSQLのインデックスアクセスメソッドとして実装されているため、 B-treeインデックスなど他の種類のインデックスと同じように、 あるいは他の種類のインデックスと組み合わせて使うことができます。 検索は追加定義の「@@」演算子を用いて行います。 PostgreSQL8.3では、「@@」演算子の代わりに「%%」演算子を用います。 また、テーブルにレコードの追加、更新、削除を行った際は、 インデックス側の情報も自動的に更新されます。
スコアを利用したクエリ文
全文検索エンジンの検索スコア(検索内容との合致度)をクエリ中で取得し、 フィルタ条件やソート条件として使用することができます。

ライセンス

LudiaはOSS(オープンソースソフトウェア)です。 あなたは、Free Software Foundationが公表した GNU Lesser General Public Licenseのバージョン2.1が定める条項に従って、 本プログラムを再頒布または変更することができます。 頒布にあたっては、 市場性及び特定目的適合性についての暗黙の保証を含めて、 いかなる保障も行いません。 詳細は GNU LESSER GENERAL PUBLIC LICENSE Version 2.1 をお読みください。

制限事項

  • 一意性インデックスの機能は提供しません。
  • WALに対応していません。 クラッシュリカバリやPITRを実行した場合、 REINDEXが必要になります。
  • DROP INDEXを実行すると、Sennaのインデックスファイルが残ります。 ( インデックスの削除 の節に削除方法があります。)
  • (Ludiaのインデックスによる)CLUSTERには対応していません。
  • シーケンシャルスキャンを行う場合は一部制限があります。 シーケンシャルスキャンの抑制 の節に詳細な説明があるので参照してください。
  • テキストフィルタとしてTextPorterを使用する場合、 別途購入する必要があります。 ( TextPorter Ver.4 Copyright(c) 1999-2007 Antenna House, Inc. )
  • PostgreSQL8.3ではVACUUMに対応しておりません。VACUUM後にREINDEXを実行してください。

動作環境

以下の環境で動作確認をしています。

OS:RedHat Enterprise Linux AS[ES] 4
DBMS:PostgreSQL 8.2.9 (8.3.3, 8.1.13)
Senna:1.1.3 (1.1.1以前のバージョンには対応していません)
MeCab:0.97

連絡先

バグ報告や技術的な質問については、 Ludia-usersメーリングリスト でお問い合わせください。

インストール

インストール方法については、 このファイルと同じディレクトリにあるINSTALLを参照してください。

バージョンアップ

MeCab, MeCab辞書, Sennaのバージョンに変更がない場合は、 既存のインデックスをそのまま利用できます。 Ludiaを上書きでインストールした後に、 インデックスアクセスメソッドの登録設定ファイルの編集 を行い、 pg_ctl restartコマンドでデータベースサーバを再起動してください。

インデックスを再構築するする必要があるのは、以下のような場合です。

この場合には、 利用中のバージョンの アンインストールスクリプトを実行し、 環境をクリーンアップしてください。 (スクリプトを実行することで、LudiaのインデックスはすべてDROPされます。)

$ psql -f /usr/local/pgsql/share/uninstall_pgsenna2.sql test

その後、通常の手順でインストールを行い、データベースサーバを再起動してください。

使い方

インデックスアクセスメソッドの登録

Ludiaを使用するデータベースに対してインデックスアクセスメソッドを登録します。 ソースアーカイブに含まれている pgsenna2.sql をpsqlから実行してください。 (pgsenna2.sqlはPostgreSQLのshareディレクトリにインストールされます。)

$ psql -f /usr/local/pgsql/share/pgsenna2.sql testdb

バージョンアップで既存の環境にインストールする場合には、 以下のようなエラーが表示されますが、無視して問題ありません。

ERROR:  duplicate key violates unique constraint "pg_am_name_index"
ERROR:  duplicate key violates unique constraint "pg_am_name_index"
ERROR:  duplicate key violates unique constraint "pg_am_name_index"
ERROR:  operator @@ already exists
ERROR:  operator class "text_ops" for access method "fulltext" already exists
ERROR:  operator class "text_ops" for access method "fulltextb" already exists
ERROR:  operator class "text_ops" for access method "fulltextu" already exists

設定ファイルの編集

デフォルト設定でLudiaを使用する場合は、この節を飛ばしても問題ありません。

Ludiaを使用するデータベースクラスタのpostgresql.confファイルに、 以下の設定内容を追加してください。 (custom_variable_classesの項目は必須です。 それ以外の項目は記述しないと、デフォルト値が参照されます。) 設定を反映するためにはPostgreSQLを再起動する必要があります。 postgresql.confの設定が反映されていないと、 実行時にエラーになってしまうので注意してください。 設定内容についての詳細は、 実行時の設定 の節を参照してください。

custom_variable_classes = 'ludia'
ludia.max_n_sort_result = 10000
ludia.enable_seqscan = on
ludia.seqscan_flags = 1
ludia.sen_index_flags = 31
ludia.max_n_index_cache = 16
ludia.initial_n_segments = 512

もしすでにcustom_variable_classesが設定されている場合は、 そこにludiaというクラス名を追加してください。

インデックスの作成

ここでは、例として以下のようなテーブルを利用します。

CREATE TABLE table1 (col1 text, col2 varchar(128));
INSERT INTO table1 VALUES ('すもももももももものうち', 'あの壺はよいものだ');
INSERT INTO table1 VALUES ('ももから生まれた桃太郎', 'あの壷はよいものだ');

全文検索インデックスはCREATE INDEX 文を利用して作成します。

CREATE INDEX index1 ON table1 USING fulltext(col1);

Ludiaがインデックス対象とできるのはtext型のみなので、 char型などの列に対してインデックスを作成したい場合はキャストしてください。

CREATE INDEX index2 ON table1 USING fulltextb((col2::text));

インデックスアクセスメソッド名には

  • fulltext : 正規化 + 形態素解析 (SEN_INDEX_NORMALIZE)
  • fulltextb : 正規化 + 2-gram (SEN_INDEX_NORMALIZE|SEN_INDEX_NGRAM)
  • fulltextu : ユーザ定義

の3種類があり、どれを指定するかによってSennaインデックスのフラグが変わります。 ユーザ定義(fulltextu)についての詳細は Sennaインデックス作成時のオプション の節を参照してください。

検索の実行

Ludiaのインデックスを用いた検索を行う場合には @@ 演算子を使用します。 @@ 演算子の右辺には Sennaの検索クエリ を指定してください。

SELECT * FROM table1 WHERE col1 @@ 'もも';
           col1           |        col2
--------------------------+--------------------
 すもももももももものうち | あの壺はよいものだ
 ももから生まれた桃太郎   | あの壷はよいものだ
(2 rows)

また、この検索における検索スコアを取得するためには、 pgs2getscore関数を利用します。 pgs2getscore関数は2つの引数をとります。 1番目の引数には検索対象となった行のTIDを、 2番目の引数にはインデックス名を指定してください。

SELECT col1, pgs2getscore(table1.ctid, 'index1') FROM table1 WHERE col1 @@ 'もも';
           col1           | pgs2getscore
--------------------------+--------------
 すもももももももものうち |           10
 ももから生まれた桃太郎   |            5

インデックスの削除

PostgreSQLのインデックスリレーションファイルと、 Ludiaのインデックスファイルは以下の5つから構成されます。 (テーブル空間を使用している場合は、テーブル空間定義時に指定した場所に置かれます。)

  1. PGDATA/base/データベースのOID/インデックスのファイルノード番号
  2. PGDATA/base/データベースのOID/インデックスのファイルノード番号.SEN
  3. PGDATA/base/データベースのOID/インデックスのファイルノード番号.SEN.i
  4. PGDATA/base/データベースのOID/インデックスのファイルノード番号.SEN.i.c
  5. PGDATA/base/データベースのOID/インデックスのファイルノード番号.SEN.l

1 はPostgreSQLのインデックスリレーションファイル、 2~5はSennaのインデックスファイルです。 2~5のファイルは手作業で削除する必要があります。

参考として、インデックスのファイルノード番号は以下のようなクエリで取得できます。

SELECT relfilenode FROM pg_class WHERE relname = 'index1';

また、データベースのOIDは以下のようなクエリで取得できます。

SELECT oid FROM pg_database WHERE datname = 'dbname';

1のファイルについては、DROP INDEXを実行することで削除されます。

DROP INDEX index1;

あるいは、pgs2destroy関数を利用すると、 データベース中の不要になったSennaインデックスファイルを一括して削除できます。 pgs2destroy関数は、2~5が存在するが1のファイルが存在しない、という場合に、 2~5のファイルを削除します。

# DROP TABLE table1;
DROP TABLE

# SELECT pgs2destroy();
 pgs2destroy
-------------
           1
(1 row)

関数の返り値は、削除したインデックス数です。 (上記の2~5のファイルで1セットです。)

PostgreSQL8.3では、DROP INDEXの直後には1のファイルが消えず、 チェックポイント処理後に1のファイルが削除されます。 そのため、DROP INDEXの直後にpgs2destroy関数を実行しても、 2~5のファイルは削除されません。 pgs2destroy関数を使用する場合は、CHECKPOINTコマンドを予め実行してください。

実行時の設定

シーケンシャルスキャンの抑制

@@演算子を用いた全文検索条件を指定しても、シーケンシャルスキャンが実行された場合には、 インデックススキャンの場合と同様の検索を行うことができません。 具体的には、スコアの取得、高速ヒット関数、近傍検索 *N 、類似検索 *S ができません。 (空白で区切った複数検索キーによる検索や、Senna演算子+,-などのAND,OR検索は可能です。) また、シーケンシャルスキャンはインデックススキャンと比べて、非常に低速です。 そのためLudiaでは、シーケンシャルスキャンが実行された場合に エラーにする設定(ludia.enable_seqscan)が用意されています。 以下の例ではenable_indexscanをoffにして、 強制的にシーケンシャルスキャンを実行しています。 ludia.enable_seqscanをoffにする事により、シーケンシャルスキャンがエラーとなっています。

# SET ludia.enable_seqscan TO off;
SET

# SET enable_indexscan TO off;
SET

# EXPLAIN SELECT col1 FROM table1 WHERE col1 @@ 'もも';
                      QUERY PLAN
-------------------------------------------------------
 Seq Scan on table1  (cost=0.00..1.02 rows=1 width=32)
   Filter: (col1 @@ 'もも'::text)
(2 rows)

# SELECT col1 FROM table1 WHERE col1 @@ 'もも';
ERROR:  pgsenna2: sequencial scan disabled.

ludia.enable_seqscanをonにする事により、シーケンシャルスキャンを実行する事もできます。

# SET ludia.enable_seqscan TO on;
SET

# SELECT col1 FROM table1 WHERE col1 @@ 'もも';
           col1
--------------------------
 すもももももももものうち
 ももから生まれた桃太郎
(2 rows)

インデックスを張っていないカラムに対して@@演算子指定した場合も、 Senna演算子を利用したシーケンシャルスキャンとなります。

# SELECT col1 FROM table1 WHERE col1 @@ 'もも + 桃太郎';
           col1
--------------------------
 ももから生まれた桃太郎
(1 rows)

上記の例では、SETコマンドでludia.enable_seqscanを設定していますが、 postgresql.confで設定する事もできます。 (SETコマンドによる変更の場合、そのセッション内のみ有効です。) また、PostgreSQLの設定変数enable_seqscanと Ludiaの設定変数ludia.enable_seqscanは混同しやすいため、注意してください。

シーケンシャルスキャンの正規化

上述したように、postgresql.confのludia.enable_seqscanをonにすると、 シーケンシャルスキャンが可能となります。 その際、ludia.seqscan_flagsを1にした場合は、 シーケンシャルスキャンは比較文字列を正規化した後、スキャンを行います。 ludia.seqscan_flagsを0にした場合は、正規化せずにスキャンを実行します。

ここで言う正規化とは、 Sennaインデックス作成時のオプション の項にある、 SEN_INDEX_NORMALIZEと同様の操作を指します。

下記の例ではインデックスが張っていないカラムで検索を行っています。

# EXPLAIN SELECT * FROM tab WHERE col @@ 'test';
                       QUERY PLAN
---------------------------------------------------------
 Seq Scan on seqscan  (cost=0.00..25.38 rows=1 width=32)
   Filter: (col @@ 'test'::text)
(2 rows)

# SHOW ludia.seqscan_flags;
 ludia.seqscan_flags
---------------------
 1
(1 row)

# SELECT * FROM tab WHERE col @@ 'test';
   col
----------
 test
 TesT
 TEST
(3 rows)

検索ヒット数の上限の設定

postgresql.confのludia.max_n_sort_resultを設定していると、 検索でヒットした行のうち、スコア上位のものから max_n_sort_result件だけが返却されます。 ただし、クエリの最終的な結果セットは 必ずしもスコア順にソートされているわけではありません。 ソートが必要な場合にはORDER BYを利用してください。

# SHOW ludia.max_n_sort_result;
 ludia.max_n_sort_result
-------------------------
 10000
(1 row)

# SELECT col1, pgs2getscore(ctid, 'index1') FROM table1 WHERE col1 @@ 'もも';
           col1           | pgs2getscore
--------------------------+--------------
 すもももももももものうち |           10
 ももから生まれた桃太郎   |            5
(2 rows)

このmax_n_sort_resultによる上限はSETコマンドでも、 セッション内で一時的に変更することができます。

# SET ludia.max_n_sort_result TO 1;
SET

# SELECT col1, pgs2getscore(ctid, 'index1') FROM table1 WHERE col1 @@ 'もも';
           col1           | pgs2getscore
--------------------------+--------------
 すもももももももものうち |           10
(1 row)

また、特殊な設定として、 ludia.max_n_sort_resultを-1に設定すると上限なしという意味になります。 ただし、現状では-1に設定すると、 pgs2getscore関数によるスコアの取得が利用できなくなるという制限があります。

Sennaインデックス作成時のオプション

アクセスメソッドとしてfulltextuを選択すると、 インデックス作成時にSennaインデックスのフラグを指定することができます。 利用できるフラグは(Senna 1.0.8では)以下のような定義と意味をもっています。 (詳しくは SennaのAPIドキュメント を参照してください。)

#define SEN_INDEX_NORMALIZE                     0x0001
#define SEN_INDEX_SPLIT_ALPHA                   0x0002
#define SEN_INDEX_SPLIT_DIGIT                   0x0004
#define SEN_INDEX_SPLIT_SYMBOL                  0x0008
#define SEN_INDEX_NGRAM                         0x0010
#define SEN_INDEX_DELIMITED                     0x0020
SEN_INDEX_NORMALIZE
英文字、数字、カタカナ、記号などは全角文字/半角文字の正規化を行い、 英文字に関しては大文字/小文字を正規化した後、インデックスに登録する。
SEN_INDEX_SPLIT_ALPHA
英文字列を文字要素に分割する。 (それ以外の場合は連続した英文字列を1単語とする。)
SEN_INDEX_SPLIT_DIGIT
数字文字列を文字要素に分割する。 (それ以外の場合は連続した数字文字列を1単語とする。)
SEN_INDEX_SPLIT_SYMBOL
記号文字列も文字要素に分割する。 (それ以外の場合は、連続した記号文字列を1単語とする。)
SEN_INDEX_NGRAM
(形態素解析ではなく)N-gramを用いる。
SEN_INDEX_DELIMITED
(形態素解析ではなく)空白区切りで単語を区切る。

postgresql.confの設定には、10進数の値を指定してください。 例えば、 SEN_INDEX_NGRAM|SEN_INDEX_NORMALIZE|SEN_INDEX_SPLIT_ALPHA というフラグを指定する場合には、

ludia.sen_index_flags = 19

となります。

インデックスの同時オープン数の上限

Ludiaはインデックスを1つオープンするごとにメモリを確保します。 基本的には1度オープンしたインデックスは、 バックエンドプロセスが終了するまでクローズしません。 ただし、postgresql.confの設定変数の ludia.max_n_index_cacheで設定された値より多くの インデックスを開こうとすると、 もっとも最近利用されていないインデックスをクローズします。 現在オープンされているインデックスは pgs2indexcache関数で確認することができます。

SELECT name FROM pgs2indexcache();

インデックスの初期サイズ

postgresql.confの ludia.initial_n_segments / 4 [Mbyte] が インデックスの初期サイズとなります。 ludia.initial_n_segmentsがデフォルト(512)の場合、 転置インデックスの最大サイズは8GB程度になります。 一般的には インデックス対象の総テキスト量≒転置インデックスのサイズ となる傾向があるため、 これ以上のサイズのテキストに対してインデックスを作成する場合、 initial_n_segmentsの値を増やす必要があるかもしれません。

設定内容の表示

pgs2getoption関数を用いると、現在の設定を確認することができます。

# \x
Expanded display is on.
# SELECT * FROM pgs2getoption();
-[ RECORD 1 ]------+----
max_n_sort_result  | 10000
enable_seqscan     | on
seqscan_flags      | 1
sen_index_flags    | 31
max_n_index_cache  | 16
initial_n_segments | 512