トピックスでも触れていますが、国文学研究資料館様向けのサポートサービスで、国文学研究資料館様が運用している国書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
するだけで完了です。
削除や更新も DELETE
、UPDATE
を発行することで対応でき、データベースの操作だけでルールの追加、更新、削除が可能です。
アプリケーションの変更はいりません。
(ただし、同一視のルールを変更した場合はインデックスの再生成(REINDEX
)が必要になる点には注意してください。
インデックスを再生成しないと、変更したルールは適用されません。)
また、異体字の展開をPGroonga側で実施するので、SQLも以下のようにシンプルにできます。
(SELECT句は省略)
WHERE column_name::text &@~ '筆道';
上記のSQLで使っている &@~
は全文検索用の演算子で、 PGroongaのインデックスを使った LIKE
検索よりも高速なので
LIKE
ではなく、 &@~
を使っています。
検索キーワードは、PGroongaのインデックス内で既に定義されている同一視のルールをもとに変換され検索されます。 したがって、アプリケーションは異体字が何かというのを意識せず、入力された検索キーワードをクエリーに指定するだけでよくなります。
このようにして、PGroongaを使って異体字検索を高速化し、アプリケーションの開発効率も向上させることができました。
まとめ
ここで紹介した異体字検索のように、表記ゆれを吸収するためにたくさんの変換パターンを持っておりそれを展開して検索している場合、 上記のようにPGroongaに同一視のルールを定義する、あるいは、PGroonga(のバックエンドで動いているGroonga)が持っている同一視のルールを使うことで解決できる場合があります。 高速に表記ゆれを吸収してシンプルなSQLで検索をしたい場合は、ぜひ、PGroongaの採用を検討してみてください。