textsearchに(無理やり)近似検索を組み込んでみた

以前、作成したntext型の近似マッチ関数をなんとかtextsearchに組み込めないか、いろいろ試してみたが、結局かなり強引な方法で実装してみることにした。
PostgreSQL本体にも手を入れているので、対応方式としてはちょっと微妙だが・・・
今回はHOOKポイントを新規に作成し、HOOK関数として近似マッチ関数を組み込んでみた。

HOOKの準備

textsearch内でmemcmpを使って判定している場所を、自作の近似マッチ関数(正確には類似度産出関数と、その算出結果の判定)に置き換えれば、理屈上はなんとかなると思ったので、その場所にHOOK関数をロードできるように書き換えてみる。

(backend/utils/adt/tsvector_op.c tsCompareString()内)

                // cmp = memcmp(a, b, Min(lena, lenb));
                if (TsearchCompare_hook == NULL)
                {
                    cmp = memcmp(a, b, Min(lena, lenb));
                }
                else
                {
                    // Custom Compare Hook Functoin Call
                    cmp = (*TsearchCompare_hook) (a, b, Min(lena, lenb));
                }

もう一か所修正する。比較対象の長さが違う場合の処理でHOOK関数の近似判定で0と判定した場合は長さによる比較をスキップする。

                else if ((TsearchCompare_hook == NULL) && cmp == 0 && lena != lenb) // modify by nuko
                {
                        cmp = (lena < lenb) ? -1 : 1;
                }

そしてHOOK関数のポインタもこのソースファイル内で定義しておく。

int (*TsearchCompare_hook) (char* a, char* b, int len) = NULL; // add by nuko

HOOKにして呼び出すようにしたのは、近似マッチ方式を自作関数だけではなく、(例えば)pg_trgmのsimilality関数を使う方式に変更しても本体に変更を加えないようにするためだ。

HOOK関数の実装

呼び出し側の準備ができたので、次は呼び出されるHOOK関数を実装する。以下の形式の関数を作成し、

TsearchCompareApprox(char* a, char* b, int len)

その中で以前作成した、ntext EXTENSINOのsimilar_rate()を呼び出して、その結果(類似度)が一定以上であれば、0を、それ以下なら通常のmemcmp()を呼び出すような処理を実装する。
あとはロード時にこのHOOK関数を設定するように、_PG_init() を実装する。この関数は後述の shared_preload_libraries を設定した場合に、PostgreSQLサーバ起動時に呼ばれる。

void
_PG_init(void)
{
        /* activate hook module is loaded */
        TsearchCompare_hook = TsearchCompareApprox;
}

この関数を共有ライブラリ形式として作成する。
とりあえず、"ts_compare_approx.so"という名前でビルドしてみた。

HOOKの組み込み

作成したライブラリをHOOKとして有効にするために、以下の設定を postgresql.conf に設定する。

shared_preload_libraries = 'ts_compare_approx'

そして、PostgreSQLサーバを起動する。

$ LOG:  loaded library "ts_compare_approx"
LOG:  database system was shut down at 2013-02-10 04:39:23 PST
LOG:  database system is ready to accept connections
LOG:  autovacuum launcher started

LOGに ts_compare_approx がロードされたことが出力される。組み込み成功。

実行

こんなデータが入っているとする。

test=# SELECT id, data FROM test WHERE id = 15;
 id |                     data                      
----+-----------------------------------------------
 15 | 横シューティング御三家はダライアス、グラディウス、アールタイプだ。
(1 row)

test=# 

HOOKを組み込まないとき。当たり前だけど「ダライアヌ」ではヒットしない。

test=# SELECT id, data FROM test WHERE to_tsvector('japanese', data) @@ to_tsquery('japanese', 'ダライアヌ');
 id | data 
----+------
(0 rows)

HOOKを組み込むと・・・「ダライアヌ」でも「ダライアス」がヒットした。

test=# SELECT id, data FROM test WHERE to_tsvector('japanese', data) @@ to_tsquery('japanese', 'ダライアヌ');
 id |                     data                      
----+-----------------------------------------------
 15 | 横シューティング御三家はダライアス、グラディウス、アールタイプだ。
(1 row)

スニペットを表示する場合には、強調個所が「ダライアヌ」に代わるわけではなく、元の「ダライアス」が強調される。

test=# SELECT id, ts_headline('japanese',data, 'ダライアヌ')  FROM test WHERE to_tsvector('japanese', data) @@ to_tsquery('japanese', 'ダライアヌ'); id |                               ts_headline                               
----+-------------------------------------------------------------------------
 15 | 横シューティング御三家は<b>ダライアス</b>、グラディウス、アールタイプだ。
(1 row)

だから何?とは言わないようにw

補足

最初はHOOK関数内に自作の日本語正規化関数も組み込もうとしたのだが、textsearch_jaと併用したときに、先にMecabによる単語分割や正規化が行われるため、今一つ相性が良くなく結局は組み込むのをやめた。

修正

  • 石井達夫さんの指摘を受けて、本体側修正コードのスタイルを修正しました。(2013-02-12)
  • スニペット(ts_headline)を使ったときの例を追加しました。