LudiaはPostgreSQLに高速な全文検索機能を提供します。 全文検索エンジンSennaを利用し、データベース内のテキスト情報を高速検索します。 Ludiaは以下のような特徴をもっています。
LudiaはOSS(オープンソースソフトウェア)です。 あなたは、Free Software Foundationが公表した GNU Lesser General Public Licenseのバージョン2.1が定める条項に従って、 本プログラムを再頒布または変更することができます。 頒布にあたっては、 市場性及び特定目的適合性についての暗黙の保証を含めて、 いかなる保障も行いません。 詳細は GNU LESSER GENERAL PUBLIC LICENSE Version 2.1 をお読みください。
以下の環境で動作確認をしています。
OS: | RedHat Enterprise Linux AS[ES] 4 |
---|---|
DBMS: | PostgreSQL 8.2.4 (8.1.9) |
Senna: | 1.0.5 (1.0.4以前のバージョンには対応していません) |
MeCab: | 0.95 |
バグ報告や技術的な質問については、 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を使用するデータベースクラスタのpostgresql.confファイルに、 以下の設定内容を追加してください。 設定を反映するためにはPostgreSQLを再起動する必要があります。 postgresql.confの設定が反映されていないと、 実行時にエラーになってしまうので注意してください。 設定内容についての詳細は、 実行時の設定 の節を参照してください。:
custom_variable_classes = 'ludia' ludia.max_n_sort_result = 10000 ludia.enable_seqscan = on 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));
インデックスアクセスメソッド名には
の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 は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セットです。)
@@演算子を用いた全文検索条件を指定しても、シーケンシャルスキャンが実行された場合には、 インデックススキャンの場合と同様の検索を行うことができません。 具体的には、スコアの取得、高速ヒット関数、近傍検索 *N 、類似検索 *S ができません。 (空白で区切った複数検索キーによる検索や、Senna演算子+、-などのAND, OR検索は可能です。) そのためLudiaでは、シーケンシャルスキャンが実行された場合に エラーにする設定があります。 (以下の例ではenable_indexscanをoffにして、 強制的にシーケンシャルスキャンを実行しています。):
# 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. ERROR: pgsenna2: sequencial scan disabled.
この設定はpostgresql.confのludia.enable_seqscan変数で指定されますが、 SETコマンドでも変更することができます。 (SETコマンドによる変更はそのセッション内でのみ有効です。):
# 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)
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)
この上限はSETコマンドでも変更することができます。 (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関数によるスコアの取得が利用できなくなります。)
アクセスメソッドとしてfulltextuを選択すると、 インデックス作成時にSennaインデックスのフラグを指定することができます。 利用できるフラグは(Senna 1.0.5では)以下のような定義と意味をもっています。 (詳しくは 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
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 * 256 [Kbyte] が インデックスの初期サイズとなります。 レコード数が数百万程度に収まる場合には、 ludia.initial_n_segmentsのデフォルト値(512)で十分であり、 変更する必要はありません。
pgs2getoption関数を用いると、現在の設定を確認することができます。:
# \x Expanded display is on. # SELECT * FROM pgs2getoption(); -[ RECORD 1 ]------+---- max_n_sort_result | 10000 enable_seqscan | on sen_index_flags | 31 max_n_index_cache | 16 initial_n_segments | 512
pgs2getnhits関数を用いると、 セッション内で最後に行われたSennaの検索ヒット件数を取得することができます。:
# SELECT * FROM table1 WHERE col1 @@ 'もも'; col1 | col2 --------------------------+-------------------- すもももももももものうち | あの壺はよいものだ ももから生まれた桃太郎 | あの壷はよいものだ (2 rows) # SELECT pgs2getnhits(); pgs2getnhits -------------- 2 (1 row)
これを利用すると、ヒット件数が非常に多い場合でも、 LIMIT句と組み合わせて利用することで、高速にヒット件数を取得することができます。:
# SELECT * FROM table1 WHERE col1 @@ 'もも' LIMIT 0; col1 | col2 ------+------ (0 rows) # SELECT pgs2getnhits(); pgs2getnhits -------------- 2 (1 row)
ただし、ここで得られるヒット件数はSennaの検索結果についての値であるため、 以下に挙げるような制限があります。
Ludiaのユーティリティ関数を利用することで、PDFファイルに対してインデックスを作成することができます。 ここでは Xpdf というツールに含まれている、pdftotextというコマンドを利用します。 まずはXpdfと日本語サポートパッケージをインストールしてください。
Ludiaではpdftotextを利用するための関数が2種類用意されています、 pgs2pdftotext1関数は、PDFファイルのpathを引数としてとり、 pdftotextを呼び出してPDFファイルからテキストを取り出します。:
# select pgs2pdftotext1('/tmp/PostgresForest.pdf'); pgs2pdftotext1 ----------------- 高性能・高信頼の並列分散データベース環境を低コストで実現 複数ノード上 でそれぞれ稼動している PostgreSQL をシングルシステムイメー ジとしてユー ザに提供 PostgreSQL と互換性があるため、アプリケーション開発時に新たな トレーニン グが不要 オープンソースでのシステム構築可能性を向上 ...(省略)
また、pgs2pdftotext2関数はPDFファイルそのものをbytea型のデータとして受け取り、 (それをtmpディレクトリに一時ファイルとして書き出して) pdftotextを呼び出し、PDFファイルからテキストを書き出します。:
# select pgs2pdftotext1('\\120\\104\\106\\055\\061\\056\\064\\012...(省略)');
ここでは例として、以下のようなテーブルを使用します。:
# CREATE TABLE pdffiles (id SERIAL PRIMARY KEY, filepath text, filedata bytea); # \d pdffiles Table "public.pdffiles" Column | Type | Modifiers ----------+---------+------------------------------------------------------- id | integer | not null default nextval('pdffiles_id_seq'::regclass) filepath | text | filedata | bytea | Indexes: "pdffiles_pkey" PRIMARY KEY, btree (id)
PDFファイルは、
のいずれかの方法で格納されているとします。:
# SELECT id, filepath, substring(encode(filedata, 'hex') from 1 for 30) FROM pdffiles; id | filepath | substring ----+-------------------------+-------------------------------- 1 | /tmp/PostgresForest.pdf | 255044462d312e340a25c7ec8fa20a (1 row)
1の場合にはpgs2pdftotext1関数を、2の場合にはpgs2pdftotext2関数を利用して 関数インデックスを作成することができます。:
# CREATE INDEX pidx1 on pdffiles USING fulltextb(pgs2pdftotext1(filepath)); CREATE INDEX # CREATE INDEX pidx2 on pdffiles USING fulltextb(pgs2pdftotext2(filedata)); CREATE INDEX # \d pdffiles Table "public.pdffiles" Column | Type | Modifiers ----------+---------+------------------------------------------------------- id | integer | not null default nextval('pdffiles_id_seq'::regclass) filepath | text | filedata | bytea | Indexes: "pdffiles_pkey" PRIMARY KEY, btree (id) "pidx1" fulltextb (pgs2pdftotext1(filepath)) "pidx2" fulltextb (pgs2pdftotext2(filedata))
このインデックスを利用することで、PDFファイル中のテキストに対する検索を行うことができます。 検索を実行する際にも列名に対して関数を適用してください。 (検索の際には関数は実行されません。):
# SELECT id FROM pdffiles WHERE pgs2pdftotext1(filepath) @@ '高性能'; id ---- 1 (1 row) # SELECT id FROM pdffiles WHERE pgs2pdftotext2(filedata) @@ '高信頼'; id ---- 1 (1 row)
ここで、PDFファイルに複製不可やパスワードの設定が行われていると、 この関数はエラーを返すことに注意してください。
pgs2snippet1関数を用いると、Snippet (KWIC)を作成することができます。:
# SELECT pgs2snippet1(1, 32, 1, '<em>', '</em>', 0, '筋肉痛', '怪我もなく東京マラソンを完走したが、翌日は筋肉痛のため有給休暇を取得した。'); pgs2snippet1 ------------------------------- 、翌日は<em>筋肉痛</em>のため (1 row)
引数の詳細は以下の通りとなります。
- 引数1 - flags:
- 正規化を有効にするかしないかを指定。(する: 1、しない: 0)
- 引数2 - width:
- Snippetの長さ(バイト数)を指定。
- 引数3 - max_results:
- Snippetを作る数の上限。(現状は1を指定)
- 引数4 - defaultopentag:
- キーワードの前に付ける文字列。
- 引数5 - defaultclosetag:
- キーワードの後に付ける文字列。
- 引数6 - mapping:
- HTMLの特殊文字をエスケープするかしないかを指定。(する: -1、しない: 0)
- 引数7 - keyword:
- Snippetを作成するキーワード。複数ある場合は半角スペースで区切る。
- 引数8 - string:
- Snippet作成の対象となる文書本体の文字列。
また、以下のように用いることで、検索結果のSnippetを作成することもできます。:
# SELECT pgs2snippet1(1, 32, 1, '<em>', '</em>', -1, '筋肉痛', col1) FROM table1 WHERE col1 @@ '筋肉痛'; pgs2snippet1 ---------------------------------------------------------------- 、翌日は<em>筋肉痛</em>のため (1 row)
psg2indexinfo関数を用いると、Ludiaのインデックスの情報を取得することができます。:
# \x Expanded display is on. # SELECT * FROM pgs2indexinfo(); -[ RECORD 1 ]------+------ filename | 49650 dead_flag | 0 key_size | 6 flags | 17 initial_n_segments | 512 encoding | 3 nrecords_keys | 110 file_size_keys | 8462336 nrecords_lexicon | 2432 file_size_lexicon | 8462336 inv_seg_size | 125997056 inv_chunk_size | 13516
ここで表示されるデータは以下のような意味をもっています。
- filename :
- Sennaのインデックスファイル名
- dead_flag :
- 削除フラグ。(削除フラグあり: 1、削除フラグなし: 0) 削除フラグありの場合はpgs2destroy関数の対象となる。
- key_size :
- Ludiaの場合は6。(ctidのバイトサイズ。)
- flags :
- インデックス作成時のludia.sen_index_flagsの値。
- initial_n_segments :
- インデックス作成時のludia.initial_n_segmentsの値。
- encoding :
- インデックスのエンコード。 EUC-JPは2、UTF-8は3、SJISは4、それ以外は0。
- nrecords_keys :
- インデックスに含まれるレコード数。
- file_size_keys :
- filename.SEN のファイルサイズ[byte]。
- nrecords_lexicon :
- インデックスに含まれる単語数。
- file_size_lexicon :
- filename.SEN.l のファイルサイズ[byte]。
- inv_seg_size :
- filename.SEN.i のファイルサイズ[byte]。
- inv_chunk_size :
- filename.SEN.i.c のファイルサイズ[byte]。
pgs2version関数でLudiaのバージョンを確認することができます。:
# SELECT pgs2version(); pgs2version ------------- ludia 1.1.0 (1 row)
pgs2seninfo関数でSennaのバージョンを確認することができます。:
# SELECT version, configure_option FROM pgs2seninfo(); version | configure_option ---------+------------------ 1.0.5 | --disable-nfkc (1 row)