Develop and Download Open Source Software

View README

category(Tag) tree

file info

category(Tag)
root
file name
README
last update
2007-06-04 13:21
type
HTML
editor
co-saka
description
Ludiaの概要と、基本的な使い方についての説明です。
language
Japanese
translate

Ludia 1.1.0 README

Ludiaについて

概要

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

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

ライセンス

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

制限事項

  • 複数列インデックスとしては使用できません。
  • 一意性インデックスの機能は提供しません。
  • DROP、REINDEXを実行すると、Sennaのインデックスファイルが残ります。 ( インデックスの削除 の節に削除方法があります。)
  • (Ludiaのインデックスによる)CLUSTERには対応していません。
  • シーケンシャルスキャンを行う場合は一部制限があります。 ( シーケンシャルスキャンの抑制 の節に詳細説明があります。)
  • テキストフィルタとしてTextPorterを使用する場合、 別途購入する必要があります。 ( TextPorter Ver.4 Copyright(c) 1999-2007 Antenna House, Inc. )

動作環境

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

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コマンドでデータベースサーバを再起動してください。

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

  • MeCab, MeCab辞書のバージョンアップで分かち書きが変化した場合
  • Sennaのバージョンアップでインデックスの互換性がない変更があった場合

この場合には、 利用中のバージョンの アンインストールスクリプトを実行し、 環境をクリーンアップしてください。 (スクリプトを実行することで、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));

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

  • 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セットです。)

実行時の設定

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

@@演算子を用いた全文検索条件を指定しても、シーケンシャルスキャンが実行された場合には、 インデックススキャンの場合と同様の検索を行うことができません。 具体的には、スコアの取得、高速ヒット関数、近傍検索 *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関数によるスコアの取得が利用できなくなります。)

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

アクセスメソッドとして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
SEN_INDEX_NORMALIZE
英文字、数字、カタカナ、記号などは全角文字/半角文字の正規化を行い、 英文字に関しては大文字/小文字を正規化した後、インデックスに登録する。
SEN_INDEX_SPLIT_ALPHA
N-gramインデックスで正規化を指定した際、英文字列もN文字の要素に分割する。 (それ以外の場合は連続した英文字列を1単語とする。)
SEN_INDEX_SPLIT_DIGIT
N-gramインデックスで正規化を指定した際、数字文字列もN文字の要素に分割する。 (それ以外の場合は連続した数字文字列を1単語とする。)
SEN_INDEX_SPLIT_SYMBOL
N-gramインデックスで正規化を指定した際、記号文字列もN文字の要素に分割する。 (それ以外の場合は、連続した記号文字列を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 * 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の検索結果についての値であるため、 以下に挙げるような制限があります。

  • この方法で得られるヒット件数は、セッション内で最後に行われたSennaの検索に関するものです。 一回の問い合わせ中に複数回、同一インデックスに対する検索が行われるような場合には、 最後に行われるSennaインデックスのスキャンに関するヒット件数が得られます。
  • 問い合わせに全文検索条件以外の条件が指定されていても、反映はされません。
  • 得られるヒット件数にはUPDATEやDELETEで無効になった行も含まれています。 インデックスの更新が頻繁に行われる場合には、誤差が大きくなります。
  • 検索ヒット数の上限設定(ludia.max_n_sort_result の値)は、 ここで得られるヒット件数には反映されません。

テキストフィルタを利用する

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ファイルは、

  1. filepath列にはPDFファイルのPATHが格納され、ファイルそのものはファイルシステム上に格納する。
  2. filedata列にPDFファイルそのものをbatea型で格納する。

のいずれかの方法で格納されているとします。:

# 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ファイルに複製不可やパスワードの設定が行われていると、 この関数はエラーを返すことに注意してください。

Snippetを作成する

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)