pgunconf ゆるいテキスト検索ふたたび - n-gram応用編

Post on 22-Jun-2015

1.340 Views

Category:

Technology

9 Downloads

Preview:

Click to see full reader

DESCRIPTION

2013/7/13 PostgreSQL Unconference

TRANSCRIPT

ゆるいテキスト検索ふたたび

N-gram編ぬこ@横浜(@nuko_yokohama)

自己紹介名前:ぬこ@横浜

仕事:ラーメンレビュー副業:某通信系SI会社勤務

(趣味で)誰得なPostgreSQL拡張やってます

つくったものxml_fdw:XMLファイルのFDW

w24:二十四節気型ksj:漢数字で演算する型

あいまいtextsearch用パッチneo4j_fdw:グラフデータベースのFDW

redis_fdw 9.3対応(やりかけ)

誰得?

今回のテーマpg_bigm+近似検索

pg_bigmって?

PostgreSQL組込み用N-gram全文検索系

pg_trgnは3文字分割pg_bigmは2文字分割

NTT DATAの藤井さんと澤田さんが作っている。

公開中。(”pg_bigm”で検索)

pg_bigmの売り

日本語をサポート2文字以下の検索が速い

詳しいことは(たぶん今日来ている)藤井さんと澤田さんに

聞いてください。

さて、前回のアンカンファレンス

発表でこんなことを言った

「N-gramで近似検索は無理」と言ったな。

あれは嘘だ。

確かにN-gramだけでは無理

たとえば・・・

メロスは激怒した。セリヌンティウスも激怒した。「俺の名前はセンヌリティウス

じゃねー!」

確かにN-gramだけでは無理っす

meros=# SELECT data FROM test WHERE data LIKE likequery('セリヌンティウス') LIMIT 1; data ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ メロスには竹馬の友があった。セリヌンティウスである。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。(1 row)

meros=# SELECT data FROM test WHERE data LIKE likequery('センヌリティウス') LIMIT 1; data ------(0 rows)

ですよねー

しかし

一手間かかるが近似検索ぽいことは

実はできる。

そのためには公式リリース版のpg_bigmには機能が足りない・・・

pg_trgmにはある・・・

なので作者さんに丁重にお願いしてsimilarity関数等を

作ってGithubに上げてもらった

pg_trgmでもいいんだけどさ・・・せっかくだからpg_bigmの試験も兼ねて

澤田さん、サンクス!

similarity関数

文字列間の類似度を算出(0.0~1.0)して返却

1.0なら完全一致0.0なら全く一致しない

% 演算子

%:類似度が閾値以上なら真閾値のデフォルトは0.3

pg_bigm.similarity_limit パラメータで変更可能

こんな感じbigm=# SHOW pg_bigm.similarity_limit ; pg_bigm.similarity_limit -------------------------- 0.3(1 row)

bigm=# SELECT similarity('センヌリティウス', 'セリヌンティウス'); similarity ------------ 0.384615(1 row)

bigm=# SELECT 'センヌリティウス' % 'セリヌンティウス'; ?column? ---------- t(1 row)

bigm=# SET pg_bigm.similarity_limit = 0.4;SETbigm=# SELECT 'センヌリティウス' % 'セリヌンティウス'; ?column? ---------- f(1 row)

N-gram近似検索のやりかた

テキストから辞書をMecab等で生成。

文書も辞書もN-gramインデクスを設定。

あいまいな語と辞書を比較し、近似した語を取得。取得した語で近似検索。

辞書テーブル

・・・

シラクス

セリヌンティウス

ゼウス

・・・ 文書テーブル

メロスは激怒した・・・

きょう未明メロスは村・・・

・・・

「セリヌンティウス。・・・

・・・

 勇者は、ひどく赤面・・・

セリヌンティウス

センヌリティウス類似検索で

一番類似した「セリヌンティウス」

を取り出す

「セリヌンティウス」を使ってLIKE検索で文書テーブルを

検索する。

だいたいこんな感じ

文書から抜き出したキーワード

汎用辞書

やってみた

確かにN-gramだけでは無理っす

SELECT data FROM meros WHERE data LIKE likequery( (SELECT data FROM token WHERE data % 'センヌリティウス' ORDER BY similarity(data, 'センヌリティウス') DESC) ) LIMIT 3; data ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- メロスには竹馬の友があった。セリヌンティウスである。今は此のシラクスの市で、石工をしている。その友を、これから訪ねてみるつもりなのだ。久しく逢わなかったのだから、訪ねて行くのが楽しみである。 「そうです。帰って来るのです。」メロスは必死で言い張った。「私は約束を守ります。私を、三日間だけ許して下さい。妹が、私の帰りを待っているのだ。そんなに私を信じられないならば、よろしい、この市にセリヌンティウスという石工がいます。私の無二の友人だ。あれを、人質としてここに置いて行こう。私が逃げしまって、三日目の日暮まで、ここに帰って来なかったら、あの友人を絞め殺して下さい。たのむ、そうして下さい。」  メロスは腕に唸《うな》りをつけてセリヌンティウスの頬を殴った。(3 rows)

やったね☆

確かにN-gramだけでは無理っす

EXPLAINSELECT data FROM meros WHERE data LIKE likequery( (SELECT data FROM token WHERE data % 'センヌリティウス' ORDER BY similarity(data, 'センヌリティウス') DESC) ) LIMIT 3; QUERY PLAN -------------------------------------------------------------------------------------------- Limit (cost=56.04..60.06 rows=1 width=32) InitPlan 1 (returns $0) -> Sort (cost=44.03..44.04 rows=1 width=32) Sort Key: (similarity(token.data, 'センヌリティウス'::text)) -> Bitmap Heap Scan on token (cost=40.01..44.02 rows=1 width=32) Recheck Cond: (data % 'センヌリティウス'::text) -> Bitmap Index Scan on token_data_idx (cost=0.00..40.01 rows=1 width=0) Index Cond: (data % 'センヌリティウス'::text) -> Bitmap Heap Scan on meros (cost=12.01..16.02 rows=1 width=32) Recheck Cond: (data ~~ likequery($0)) -> Bitmap Index Scan on meros_data_idx (cost=0.00..12.01 rows=1 width=0) Index Cond: (data ~~ likequery($0))(12 rows)

インデクスもきちんと使われる

pg_trgm+近似検索簡単かつ

それなりに有効?

しかし、この方式はまだまだ欠陥がある

「インタフェース」問題ふたたび

インタフェースインタフェイス

インターフェースインターフェイス

私はこの表記派

参考:各表記の類似度インタフェース インタフェイス インターフェース インターフェイス

インタフェース 1.0 0.6 0.7 0.416667

インタフェイス 0.6 1.0 0.416667 0.7

インターフェース 0.7 0.416667 1.0 0.636364

インターフェイス 0.416667 0.7 0.636364 1.0

ということは、Similarityで閾値以上の類語をOR条件で繋げば

表記ゆれ検索もある程度カバーできる?

でも、さっきのクエリをそのまま使うとエラーに

なる・・・

エラー回避のために一番安直なのは“LIMIT 1”

を指定して1行のみ返却

こんな感じ

SELECT data FROM test WHERE data LIKE likequery( (SELECT token FROM token WHERE token % 'インタフェース' ORDER BY similarity(token, 'インタフェース') DESC) );ERROR: more than one row returned by a subquery used as an expression

SELECT data FROM test WHERE data LIKE likequery( (SELECT token FROM token WHERE token % 'インタフェース' ORDER BY similarity(token, 'インタフェース') DESC LIMIT 1) )

しかし、それでは複数の候補に展開できない

どうやって複数の候補を条件に展開するか

Point

% 演算子行から配列を生成

サブクエリText配列へのcast

ANY演算子

こんな感じSELECT data FROM test WHERE data LIKE ANY ( (SELECT array_agg(sml.token) FROM (SELECT likequery(token) AS token FROM token WHERE token % 'インタフェース' ORDER BY similarity(token, 'インタフェース') DESC LIMIT 5) as sml)::text[]);

%演算子で辞書を検索一番近いN件をORDER Byで取得その結果をarray_aggで配列化

そのサブクエリの結果をtext[]でキャスト配列をLIKE ANYで評価

こんな感じSELECT data FROM test WHERE data LIKE ANY ( (SELECT array_agg(sml.token) FROM (SELECT likequery(token) AS token FROM token WHERE token % 'インタフェース' ORDER BY similarity(token, 'インタフェース') DESC LIMIT 5) as sml)::text[]); data --------------------------------------------------------------------------- 今月号のインターフェースはコンパイラ特集だ。 実装はユーザインタフェースだけでなく、ユーザエクスペリエンスを考えねばならない。 発注元との意識ずれでインタフェイスの再設計をすることになった。 僕の考えた最強のユーザー・インターフェイスは却下された。(4 rows)

やったね☆

「インタフェース」で「インターフェース」「インタフェイス」

「インターフェイス」がヒットした。

べたにSQLを書くのはちょっと大変・・・

ラッパ関数を作ろう。

こんな感じ

CREATE OR REPLACE FUNCTION create_synonyms(keyword text, limit_num int) RETURNS text[] AS $$ SELECT array_append(array_agg(sml.token), likequery(keyword)) FROM (SELECT likequery(token) AS token FROM token WHERE token % keyword ORDER BY similarity(token, keyword)  DESC LIMIT limit_num) as sml; $$ LANGUAGE sql;

ラッパ関数を使うとちょっとだけ

シンプルに記述できる

こんな感じ

SELECT data FROM test WHERE data LIKE ANY ((SELECT create_synonyms('インタフェース', 5))::text[]); data ------------------------------------------------------------------------ 今月号のインターフェースはコンパイラ特集だ。 実装はユーザインタフェースだけでなく、ユーザエクスペリエンスを考えねばならない。 発注元との意識ずれでインタフェイスの再設計をすることになった。 僕の考えた最強のユーザー・インターフェイスは却下された。

やったね☆

あのさぁ・・・(棒読み)

色々やってるけどそれくらいなら

LIKE '%インタ%ス%'で出来るんじゃないの?

そうかもしれないけど・・・

でも、メタ文字を書かせたら負けな気がする。

それに・・・

「カレーライス」で「ライスカレー」を検索できるのか?

これはLIKEの書き方では無理なはず。

こんな感じ

SELECT data FROM test WHERE data LIKE ANY ((SELECT create_synonyms('ライスカレー'))::text[]); data ------------------------------------------------------------ 彼女は言った。「カレーライスが嫌いな男子なんていません!」 昨晩食べたライスカレーはとても美味かった。(2 rows)

更なる拡張

Mecab辞書(ipadic)にはありがたいことに

読みがなもついている。

あいまい語↓

類義語 or 読みがな

これも展開するとさらにゆるく検索できないか?

読みがな側も類似検索すれば、かなのtypoにも

対応できるかも。

やってみた

こんな感じSELECT * FROM test WHERE data LIKE ANY ((SELECT create_synonyms2('小田原市'))::text[]); data -------------------------------------------------------------- 神奈川の西部には小田原ラーメンというカテゴリの醤油ラーメンがある。 源平の戦いの英雄の一人、那須与一の郷は栃木の大田原市である。 私はコーディングスタイルには然程こだわらないようにしている。 僕は小市民の星を目指している。面倒事は勘弁してくれ。(4 rows)

(゜Д゜)ハァ?

ちょっと何故ヒットしてるかわかんないですけどwww

教訓高度に(?)発達した

「だいたいあってる」は「だいたいあってない」

と区別がつかない

まとめ

pg_bigmのsimilarityと辞書を使うことで、

完全一致でない検索が出来る。

なのでpg_bigmにsimilarity機能が正式に取り込まれる

といいなあ・・・

このゆるい検索の肝はTEXT配列を返却する関数さえあればOK

ということ

汎用のシソーラス辞書を使うも良し

Web-APIでシソーラスを引っ張ってもOK

(TEXT配列さえ返却すれば)

最後に

本当は辞書的なものは使いたくない。

辞書なしでキーワードと文書間でsimilarityを考慮した評価が出来るの

が一番いいのかも。

ご清聴ありがとうございました

top related