------------------
Ludia 1.0.0 README
------------------

.. contents:: 目次


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 をお読みください。


制限事項
--------

- 複数列インデックスとしては使用できません。

- 一意性インデックスの機能は提供しません。

- VACUUMには対応していません。
  VACUUM FULL後にテーブルを更新すると、
  インデックスとテーブルの内容の整合性が取れなくなる場合があります。
  VACUUM FULLを行った場合には、インデックスを再構築してください。

- DROP、REINDEXを実行すると、Sennaのインデックスファイルが残ります。
  ( インデックスの削除_ の節に削除方法があります。)

- (Ludiaのインデックスによる)CLUSTERには対応していません。

- シーケンシャルスキャンを行う場合は一部制限があります。
  ( シーケンシャルスキャンの抑制_ の節に詳細説明があります。)


動作環境
--------

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

:OS: RedHat Enterprise Linux AS[ES] 4
:DBMS:  PostgreSQL 8.2.3 (8.1.8)
:Senna: 1.0.1
:MeCab: 0.93


連絡先
------

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

.. _Ludia-usersメーリングリスト:  http://lists.sourceforge.jp/mailman/listinfo/ludia-users



インストール
============

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



使い方
======

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

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

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


設定ファイルの編集
------------------

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

もしすでに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の検索クエリ_ を指定してください。

.. _Sennaの検索クエリ : http://qwik.jp/senna/query.html

::

  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)


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

Ludiaのデフォルトの設定では、
検索でヒットした行をスコアが高い順に
postgresql.confのludia.max_n_sort_resultで設定された行数まで返却します。::

  # 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)


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

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

.. _SennaのAPIドキュメント : http://qwik.jp/senna/APIJ.html

::

  #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文字の要素に分割する
  (それ以外の場合は連続した英文字列を１単語とする)

SEN_INDEX_SPLIT_DIGIT
  N-gramインデックスで正規化を指定した際、数字文字列もN文字の要素に分割する
  (それ以外の場合は連続した数字文字列を１単語とする)

SEN_INDEX_SPLIT_SYMBOL
  N-gramインデックスで正規化を指定した際、記号文字列もN文字の要素に分割する
  (それ以外の場合は、連続した記号文字列を１単語とする)

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

となります。



使い方(応用編)
==============

ヒット件数を高速に取得する
--------------------------

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 の値)は、
  ここで得られるヒット件数には反映されません。


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

.. _Xpdf : http://www.foolabs.com/xpdf/

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 :
    senna.confに記されたINITIAL_N_SEGMENTSの値。
    デフォルト値は512。
  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.0.0
  (1 row)
