pg_bigmとpg_trgmのsimilarity関数

アイエエエエエエ! ドウイツ!? ドウイツナンデ!?

ということで、pg_bigmのsimilarityをちょっと動かしていたらpg_bigmとpg_trgmのsimilarity関数に共通する、一見、ちょっとびっくりする挙動を見つけたのでメモっておく。
結論から言うと、別にバグっているわけじゃない。

similarity関数の暫定サポート

先日のhttp://www.postgresql.jp/wg/shikumi/shikumi26/の講演の中で、「pg_trgmにあるような類似検索用の機能ってサポートしないの?」という質問を澤田さんにしたら「ニーズがあれば作るよ〜」と回答もらったんだけど、なんと早速数日後にはsimilarity関数の、早速pg_bigm用のsimilarity関数のプロトタイプがGithubリポジトリのbranchに上がっていた。
このへんのフットワークの軽さはホント見習わないとな〜。

インストールまで

きちんとテストはしてないバージョンだよ、とは言われたけど、質問(&要望)した身としては、やはり試しておかねばなるまい。
ということでDLして動かしてみようとしたが、いきなりCREATE EXTENSIONで躓く。(´・ω・`)
原因はすぐに解ったのでささっと修正(pg_bigm--1.0.sql内の追加CREATE OPERATORのPROCEDUREに設定した関数をsimilarity_opに修正)して登録しておく。
もちろん澤田さんにもTwitter経由で連絡しておく。連絡するとすぐに修正版をGithub上にアップデートしてくれた。

similarity関数をとりあえず動かす

ということで、とりあえず動かしてみるとした。
手持ちの環境上にあるPostgreSQL 9.2.4上のデータベースにCREATE EXTENSIONで登録。
で、登録されたpg_bigm extensionの確認。

$ psql bigm
psql (9.2.4)
Type "help" for help.

bigm=# \dx
                      List of installed extensions
  Name   | Version |   Schema   |              Description
---------+---------+------------+---------------------------------------
 pg_bigm | 1.0     | public     | text index searching based on bigrams
 plpgsql | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)

bigm=# \dx+ pg_bigm
                                  Objects in extension "pg_bigm"
                                        Object Description
--------------------------------------------------------------------------------------------------
 function bigmtextcmp(text,text)
 function gin_bigm_compare_partial(text,text,smallint,internal)
 function gin_bigm_consistent(internal,smallint,text,integer,internal,internal,internal,internal)
 function gin_extract_query_bigm(text,internal,smallint,internal,internal,internal,internal)
 function gin_extract_value_bigm(text,internal)
 function likequery(text)
 function pg_gin_pending_stats(regclass)
 function show_bigm(text)
 function similarity(text,text)
 function similarity_op(text,text)
 operator %(text,text)
 operator class gin_bigm_ops for access method gin
 operator class gin_trgm_ops for access method gin
 operator family gin_bigm_ops for access method gin
 operator family gin_trgm_ops for access method gin
(15 rows)

functionとしてsimilarity()が入ってますね。関数形式はpg_trgmともちろん同じです。

ということで、まずは簡単にテスト。

bigm=# SELECT similarity('ABCDE','ABCDE');
 similarity
------------
          1
(1 row)

bigm=# SELECT similarity('ABCDE','FGHIJ');
 similarity
------------
          0
(1 row)

うむ。完全一致なら1、全く類似度がなければ0。これはわかり易い例。
じゃあ日本語ならどうなのか。

bigm=# SELECT similarity('あいうえお','あいうえお');
 similarity
------------
          1
(1 row)

うむ。完全に同一だからOK。

bigm=# SELECT similarity('あいうえお','かきくけこ');
 similarity
------------
  0.0909091
(1 row)

おや・・・この2つの文字列間には全く類似度がないと思うのだが、0にはならないのか。これはマルチバイト文字固有の挙動なのか?

bigm=# SELECT similarity('PostgreSQL','Oracle');
 similarity
------------
          0
(1 row)

bigm=# SELECT similarity('PostgreSQL','Oracle');
 similarity
------------
     0.1875
(1 row)

算出された値は十分に小さいので、よほど閾値を小さくしなければsimilarity_opで変な判定をすることはないのかもしれないが、ちょっと気になる。

で、これはpg_bigm固有かと思いきやpg_trgmでも類似の挙動になっていた。実際コードを見てもpg_bigmとpg_trgmのsimilarityや、そこから呼ばれるcnt_smlもほぼ同一だからなあ・・・。

追記(重要)

上記の挙動は私の環境構築がミスっていたのが原因。
データベースエンコーディングをutf8ではなくSQL_ASCIIで作っていた・・・。
utf8でデータベースを作成しなおして、再検証したところ全く異なるマルチバイト文字列間を比較した場合にはきちんと0が返却された。

bigm=# SELECT similarity('あああああ','ああああ');
 similarity
------------
          1
(1 row)

bigm=# SELECT similarity('あいうえお','かきくけこ');
 similarity
------------
          0
(1 row)

連続した同一文字の扱い

で、もっと気になったのは連続した同一文字が続いた文字列同士をsimilarityにかけたときの挙動。

bigm=# SELECT similarity('アイエエエエエエ!','アイエエ!');
 similarity
------------
          1
(1 row)

アイエエエエエエ! ドウイツ!? ドウイツナンデ!?
同一文字が(2文字以上?)連続した場合、何文字あっても2文字だけ連続した文字と解釈しているのかな。

ちなみにこれもpg_bigm/pg_trgmともに同じような挙動を示す(pg_trgmの場合3文字以上、pg_bigmの場合は2文字以上みたい。語を3文字区切り/2文字区切りで処理する違いからくるのだろう)。
そして、この挙動は日本語固有というわけでもなく英字でも同様である。

trgm=# SELECT similarity('Yahooooooooo!','Yahooo!');
 similarity
------------
          1
(1 row)

show_bigmの挙動

で、なんで上記のような挙動になるのかというと、それはshow_bigm()を動かしてみると理解できる。show_bigmで上記の文字列を指定すると

bigm=# SELECT show_bigm('アイエエエエエエ!');
             show_bigm
-----------------------------------
 {アイ,イエ,エエ,エ!,"! "," ア"}
(1 row)

bigm=# SELECT show_bigm('アイエエ!');
             show_bigm
-----------------------------------
 {アイ,イエ,エエ,エ!,"! "," ア"}
(1 row)

全く同じ結果になる。つまりsimilarity関数では、このbigm結果を比較しているので全く同一と見なすという挙動なのだろう。
これはpg_trgmでも同様(こっちはハッシュ値が同じ。末端の"cf"がよく分からんが)だった。

trgm=# CREATE EXTENSION pg_trgm ;
CREATE EXTENSION
trgm=# SELECT show_trgm('アイエエエエエエ!');
                          show_trgm
-------------------------------------------------------------
 {0xb6ea15,0xcadd17,0xcfb0ce,0xeaa498,0x08dcbf,0x2ebd32,cf;}
(1 row)

trgm=# SELECT show_trgm('アイエエ!');
                        show_trgm
---------------------------------------------------------
 {0xb6ea15,0xcadd17,0xcfb0ce,0xeaa498,0x08dcbf,0x2ebd32}
(1 row)

おまけ

で、この検証やってるときに気づいたんだけど、pg_bigmでsimilarity()/similarity_op()をサポートするなら合わせてshow_limit()/set_limit()も必要かも・・・。