PostgreSQL 9.6 + textsearch_ja


ぬこは激怒した。
未だに9.0以降公式に対応されていない textsearch_ja をなんとかせねばと考えた。

ということで、9.6 beta1も出てきたことだし、そろそろ textsearch_ja を試してみようかと思ったわけです。

PostgreSQL 9.6 全文検索

久々にPostgreSQL全文検索(full text search)に新機能が入ったみたい。
今回入った機能はフレーズ検索。簡単に言えば語順を意識した検索だ。

例えば、以下のようなテキストがあるとする。

tsearch=# SELECT data FROM animal;
                       data                       
--------------------------------------------------
 I love cats.
 I like cats and dogs.
 In my bed, four dogs and five cats are sleeping.
 In my room, rats and cats are fighting.
 Rabbit is cute. However, cats are more cute.
 Lion is a big cat. Cat is a small lion.
 Miss Mery's sheep is very cute.
 Miss Magee's dog is very strong.
 Oscar's doll is very experimental.
(9 rows)

これを以下のような全文検索関数を使って検索してみる。

tsearch=# SELECT 
  data 
FROM animal 
WHERE to_tsvector('english', data) @@ to_tsquery('english', 'dog');
                       data                       
--------------------------------------------------
 I like cats and dogs.
 In my bed, four dogs and five cats are sleeping.
 Miss Magee's dog is very strong.
(3 rows)

はい。dogを含むテキストが検索されましたね。
マギーさんの犬は本当は犬じゃない気もしますが気にしないこと。

では次に、dogとcatを含むテキストを検索してみます。

tsearch=# SELECT 
  data 
FROM animal 
WHERE to_tsvector('english', data) @@ to_tsquery('english', 'dog|cat');
                       data                       
--------------------------------------------------
 I love cats.
 I like cats and dogs.
 In my bed, four dogs and five cats are sleeping.
 In my room, rats and cats are fighting.
 Rabbit is cute. However, cats are more cute.
 Lion is a big cat. Cat is a small lion.
 Miss Magee's dog is very strong.
(7 rows)

dog または cat を含むテキストが検索されました。
ただ、結果を見てもわかるように、dogとcatの語順は関係なく、単にどちらかを含むテキストが検索されています。

で、9.6から追加されたフレーズ検索は、語順を明示的に指定できるっぽい。
フレーズ検索は tsquery_phrase() という関数を使います。先行する語、後行する語、そして語間を数値で指定する。
こんな感じ。まずは dog, cat の順で。

tsearch=# SELECT 
  data
FROM animal                                
WHERE                                 
  to_tsvector('japanese', data) @@                                 
  tsquery_phrase(                                
    to_tsquery('japanese', 'cat'), to_tsquery('japanese', 'dog'), 10);
         data          
-----------------------
 I like cats and dogs.
(1 row)

次に、 cat, dog の順で。

tsearch=# SELECT 
  data
FROM animal
WHERE 
  to_tsvector('japanese', data) @@ 
  tsquery_phrase(
    to_tsquery('japanese', 'dog'), to_tsquery('japanese', 'cat'), 10);
                       data                       
--------------------------------------------------
 In my bed, four dogs and five cats are sleeping.
(1 row)

なお、上記の検索結果だと dog と cat の語間は2つ空いている。
なので、3番目の引数を2以下にするとヒットしなくなる。

tsearch=# SELECT 
  data
FROM animal
WHERE 
  to_tsvector('japanese', data) @@ 
  tsquery_phrase(
    to_tsquery('japanese', 'dog'), to_tsquery('japanese', 'cat'), 3);
                       data                       
--------------------------------------------------
 In my bed, four dogs and five cats are sleeping.
(1 row)

tsearch=# SELECT 
  data
FROM animal
WHERE 
  to_tsvector('japanese', data) @@ 
  tsquery_phrase(
    to_tsquery('japanese', 'dog'), to_tsquery('japanese', 'cat'), 2);
 data 
------
(0 rows)

しかし英語で検索なんて、普段俺はしないしなー。やっぱり日本語で検索したくなる。
そこで textsearch_ja ですよ。

textsearch_ja のインストール

さて、textsearch_ja なんですが、残念なことに、公式にはPostgreSQL 9.0 対応以降、更新されてません。

textsearch_jaや依存ソフトウェアのMecabのインストールについては下記参照。ということで割愛。

なお、いつものように、公式サイトからダウンロードしたものだと、そのままではビルド出来ないので

をやってビルド。

補足:ヘッダファイルの追加。これをtextsearch_ja.cに追加する。
#include "access/htup_details.h" /* add */


一応これでビルドとインストールはできた。
本当は、インストールスクリプトをCREATE EXTENSION対応しないとなー、と思いつつ、どうせ自分しか使わないので、いま一つモチベーションが起きずw

ということで、いつものように「走れメロス」をサンプルにします。

基本機能を動かしてみた

まずはメロス君を検索してみます。

tsearch=# SELECT 
  ts_headline('japanese', data, 
    to_tsquery('japanese', 'メロス'))
FROM meros 
WHERE to_tsvector('japanese', data) @@ to_tsquery('japanese', 'メロス') LIMIT 10;
                                                                  ts_headline                                                                  
-----------------------------------------------------------------------------------------------------------------------------------------------
 走れ<b>メロス</b>
 <b>メロス</b>は激怒した。必ず、かの邪智暴虐じゃちぼうぎゃくの王を除かなければならぬと決意した。<b>メロス</b>には政治
 <b>メロス</b>は村を出発し、野を越え山越え、十里はなれた此このシラクスの市にやって来
 <b>メロス</b>には竹馬の友があった。セリヌンティウスである。今は此のシラクスの市で、石工をしている。その友を、これから
  聞いて、<b>メロス</b>は激怒した。「呆あきれた王だ。生かして置けぬ。」
 <b>メロス</b>は、単純な男であった。買い物を、背負ったままで、のそのそ王城にはいって行った。たちまち彼は、巡邏じゅんらの警吏に捕縛された。調べ
 「市を暴君の手から救うのだ。」と<b>メロス</b>は悪びれずに答えた。
 <b>メロス</b>は、いきり立って反駁はんばくした。「人の心を疑うのは、最も恥ずべき悪徳だ。王は、民の忠誠をさえ疑っ
 「なんの為の平和だ。自分の地位を守る為か。」こんどは<b>メロス</b>が嘲笑した。「罪の無い人を殺して、何が平和だ。」
 <b>メロス</b>は足もとに視線を落し瞬時ためらい、「ただ、私に情をかけたいつもりなら、処刑までに三日間の日限を与えて下さい
(10 rows)

うん。大丈夫そうですね。

じゃあ、9.6で入ったフレーズ検索が動くかどうか試してみましょうか。
まず、センヌリティウス・・・じゃなくて、セリヌンティウス, メロス の語順で検索してみましょう。

tsearch=# 
tsearch=# SELECT 
  ts_headline(
    'japanese',
    data,
    tsquery_phrase(
      to_tsquery('japanese', 'セリヌンティウス'), 
      to_tsquery('japanese', 'メロス'), 10) )
FROM meros 
WHERE to_tsvector('japanese', data) @@ tsquery_phrase(to_tsquery('japanese', 'セリヌンティウス'), to_tsquery('japanese', 'メロス'), 10)
;
                                                                                 ts_headline                                                                   
               
---------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------
 <b>セリヌンティウス</b>は無言で首肯うなずき、<b>メロス</b>をひしと抱きしめた。友と友の間は、それでよかった。<b>セリヌンティウス</b>は、縄打た
 <b>セリヌンティウス</b>様の弟子でございます。」その若い石工も、<b>メロス</b>の後について走りながら叫んだ。「もう、駄目でございます。むだでございます。走るのは
、やめて下さい
 <b>セリヌンティウス</b>は、徐々に釣り上げられてゆく。<b>メロス</b>はそれを目撃して最後の勇、先刻、濁流を泳いだように群衆を掻き
 <b>セリヌンティウス</b>。」<b>メロス</b>は眼に涙を浮べて言った。「私を殴れ。ちから一ぱいに頬を殴れ。私は、途中で一度、悪い
(4 rows)

お、大丈夫そう。
じゃあ、次は メロス セリヌンティウス の順で。

tsearch=# SELECT 
  ts_headline(
    'japanese',
    data,
    tsquery_phrase(
      to_tsquery('japanese', 'メロス'), 
      to_tsquery('japanese', 'セリヌンティウス'), 10) )
FROM meros 
WHERE to_tsvector('japanese', data) @@ tsquery_phrase(to_tsquery('japanese', 'メロス'), to_tsquery('japanese', 'セリヌンティウス'), 10)
;
                                                                 ts_headline                                                                 
---------------------------------------------------------------------------------------------------------------------------------------------
 <b>メロス</b>には竹馬の友があった。<b>セリヌンティウス</b>である。今は此のシラクスの市で、石工をしている。その友を、これから
 <b>メロス</b>は、友に一切の事情を語った。<b>セリヌンティウス</b>は無言で首肯うなずき、<b>メロス</b>をひしと抱きしめた。友と友の間は、それで
  <b>メロス</b>は腕に唸うなりをつけて<b>セリヌンティウス</b>の頬を殴った。
(3 rows)

きちんと、さっきと違う結果になりましたね。殴られたセリヌンティウスも満足なことでしょう。

ランク算出もとりあえずは大丈夫っぽい。

tsearch=# SELECT 
  ts_rank_cd(
    to_tsvector('japanese', data), 
    tsquery_phrase(
      to_tsquery('japanese', 'セリヌンティウス'), 
      to_tsquery('japanese', 'メロス'), 10) ),
  ts_headline('japanese', data, 
    tsquery_phrase(
      to_tsquery('japanese', 'セリヌンティウス'), 
      to_tsquery('japanese', 'メロス'), 10) )
FROM meros 
WHERE to_tsvector('japanese', data) @@ tsquery_phrase(to_tsquery('japanese', 'セリヌンティウス'), to_tsquery('japanese', 'メロス'), 10)
;
 ts_rank_cd |                                                                                 ts_headline                                                      
                            
------------+--------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------
       0.05 | <b>セリヌンティウス</b>は無言で首肯うなずき、<b>メロス</b>をひしと抱きしめた。友と友の間は、それでよかった。<b>セリヌンティウス</b>は、縄打た
  0.0166667 | <b>セリヌンティウス</b>様の弟子でございます。」その若い石工も、<b>メロス</b>の後について走りながら叫んだ。「もう、駄目でございます。むだでござい
す。走るのは、やめて下さい
       0.02 | <b>セリヌンティウス</b>は、徐々に釣り上げられてゆく。<b>メロス</b>はそれを目撃して最後の勇、先刻、濁流を泳いだように群衆を掻き
        0.1 | <b>セリヌンティウス</b>。」<b>メロス</b>は眼に涙を浮べて言った。「私を殴れ。ちから一ぱいに頬を殴れ。私は、途中で一度、悪い
(4 rows)

残りの課題

あとは、自作のゆるい textsearch_ja 用評価関数の組み込みと、本体へのパッチだな・・・。
9.5でも動いたから、たぶん、9.6でも動くんじゃないかーと期待。