ククログ

株式会社クリアコード > ククログ > 事例紹介 - PGroongaで異体字検索をいい感じに!

事例紹介 - PGroongaで異体字検索をいい感じに!

PGroongaサポートサービスを担当している堀本です。

トピックスでも触れていますが、国文学研究資料館様向けのサポートサービスで、国文学研究資料館様が運用している国書DBの改良を行いました。 どんな問題があって、どんな改良をしたかについては、トピックスに記載のある動画で紹介していますので、そちらを見ていただければと思います。

この記事では、動画で紹介しきれなかった問題点、解決策の詳細について記載します。

問題点の詳細

まずは、問題点からです。 国書DBの問題点は、異体字の検索が遅いことでした。 なぜ遅いかは動画では、「インデックスを使っていないから」と解説しています。

では、なぜインデックスを使っていなかった(使えなかった)のでしょうか?

デフォルトのPostgreSQLで素朴に全文検索をする場合、 LIKE 演算子と % を使った中間一致になります。 つまり、 SELECT * FROM table_name WHERE column_name LIKE '%search_keyword%' のようなSQLを書くことになります。

しかし、デフォルトのPostgreSQLの場合 LIKE% を使った中間一致検索はシーケンシャルサーチになります。 (拡張機能を使えば、LIKE% を使った中間一致検索でもインデックスを使えますが、拡張機能を使わないPostgreSQLではインデックスを使えません。) つまり、データ量が多くなればなるほどパフォーマンスは落ちていきます。

国書DBで異体字の検索を行う場合は、 異体字のパターンごとに LIKE% を使った中間一致の条件をORで繋いでいく実装になっていました。

どういうことかというと、例えば"筆道"というキーワードで検索した場合を考えます。

筆の異体字は以下が定義されているとします。

"筆":["笔"]

道の異体字は以下が定義されているとします。

"道":["衜","衟","噵"]

上記の条件で、"筆道"というキーワードで検索した場合、WHERE句の条件は以下のようになっていました。

(SELECT句は省略)
WHERE column_name LIKE '%筆道%'
      OR column_name LIKE '%筆衜%'
      OR column_name LIKE '%筆衟%'
      OR column_name LIKE '%筆噵%'
      OR column_name LIKE '%笔道%'
      OR column_name LIKE '%笔衜%'
      OR column_name LIKE '%笔衟%'
      OR column_name LIKE '%笔噵%';

異体字の組み合わせのパターンは2x4で8通りあり、この8通りに対して LIKE% で中間一致を行い、それらの結果を OR しています。 前述の通り、 LIKE% を使った中間一致検索はシーケンシャルサーチなので、データ量が多く、異体字の定義が多い文字が検索された場合は、かなり重い処理になることが想像できるのではないでしょうか?

事実、異体字の数が多いキーワードでは、PostgreSQLがサーバーのCPUリソースを食いつぶしクエリーの応答を返せなくなることもありました。

解決策の詳細

次は、解決策の詳細について記載します。

問題はインデックスを使っていないことでした。 ということは、インデックスが使えるようになれば問題は解決するはずです。 「問題点の詳細」にも記載しましたが、PostgreSQLは拡張機能を使えば LIKE 演算子と % を使った中間一致でもインデックスを使えます。

では、日本語の全文検索を高速にできるPostgreSQLの拡張とはなんでしょうか?

そうですね、PGroongaです。

PGroonga を導入すると LIKE 演算子と % を使った中間一致でもインデックスを使えるようになるので、PGroongaを導入してPGroongaのインデックスを設定すれば 既存の実装をいじらなくても速度については解決できるはずです。

ただ、既存の実装のままにした場合、異体字のパターンが増える度にWHERE句の条件が肥大化していくことになりますし、アプリケーションの改修も都度必要になります。

異体字の展開は、キーとなる文字(検索キーワードの文字)をもとに、その文字の異体字を検索する操作です。 こういった操作は、アプリケーションで実装するのではなく、できればデータベース内で完結していて欲しいものです。

ということで、検索速度の向上だけでなく、今後、アプリケーションの改良がしやすくなるような変更をすることにしました。

PGroongaには(正確にはPGroongaのバックエンドで動作しているGroongaには)、正規化という機能があります。 この文脈での正規化は、「同じ意味を持つが形が異なる文字を統一する操作」と考えてください。 例えば、「バイオリン」と「ヴァイオリン」は形(字面)は異なりますが意味は同じです。もちろん異体字も形は違いますが同じ意味の文字です。 PGroongaは、これらを検索時やインデックス作成時に、ある1つの字面に統一して検索したり、インデックスを構築したりします。 これにより、ユーザーの表記ゆれを吸収して検索したり、旧字体を含む文書の検索もできるようになります。

世の中には、いろいろなパターンの「形(字面)は異なるが意味は同じ」文字が存在します。 PGroongaは、これらすべてのパターンに対応しているわけではありませんが、自分で独自に「形(字面)は異なるが意味は同じ」パターンを 定義して正規化できる仕組みを用意しています。

例えば、"筆"の異体字を "筆":["笔"] と定義するなら、 "筆"と"笔"を同一視するルールが必要です。 このような、どの文字(列)とどの文字(列)を同一視するかを自分で定義できます。

どのように定義して、どう使うかについては、 PGroongaの公式ドキュメント - NormalizerTable の使い方 に記載があるのでそちらを参照してください。

上記ドキュメントを参照すると、この同一視のルールの定義は特殊なものではなく、PostgreSQLの通常のテーブルとして管理していることがわかると思います。

つまり、同一視のルールが増えたとしても、新たなルールをテーブルに INSERT するだけで完了です。 削除や更新も DELETEUPDATE を発行することで対応でき、データベースの操作だけでルールの追加、更新、削除が可能です。 アプリケーションの変更はいりません。 (ただし、同一視のルールを変更した場合はインデックスの再生成(REINDEX)が必要になる点には注意してください。 インデックスを再生成しないと、変更したルールは適用されません。)

また、異体字の展開をPGroonga側で実施するので、SQLも以下のようにシンプルにできます。

(SELECT句は省略)
WHERE column_name::text &@~ '筆道';

上記のSQLで使っている &@~ は全文検索用の演算子で、 PGroongaのインデックスを使った LIKE 検索よりも高速なので LIKE ではなく、 &@~ を使っています。

検索キーワードは、PGroongaのインデックス内で既に定義されている同一視のルールをもとに変換され検索されます。 したがって、アプリケーションは異体字が何かというのを意識せず、入力された検索キーワードをクエリーに指定するだけでよくなります。

このようにして、PGroongaを使って異体字検索を高速化し、アプリケーションの開発効率も向上させることができました。

まとめ

ここで紹介した異体字検索のように、表記ゆれを吸収するためにたくさんの変換パターンを持っておりそれを展開して検索している場合、 上記のようにPGroongaに同一視のルールを定義する、あるいは、PGroonga(のバックエンドで動いているGroonga)が持っている同一視のルールを使うことで解決できる場合があります。 高速に表記ゆれを吸収してシンプルなSQLで検索をしたい場合は、ぜひ、PGroongaの採用を検討してみてください。