これは はてなエンジニアアドベントカレンダー2023 3日目の記事です。
昨日は id:pokutuna さんの
でした。私も若い頃に同僚とGitHub上で白熱してしまい観光名所になってしまっていたような気がします。気を付けていきましょう。
さて、この記事では SQLiteでLinderaを使った日本語全文検索をする話を紹介します。
モチベーション
上の記事でも話題になっているように個人開発ではDBのコストは問題です。同様に全文検索したいときにもコストに頭を悩ませているのではないでしょうか?
たとえば Amazon OpenSearch Service は t2.micro でも東京リージョンでは $20.44 /月/台 から 、冗長化には最低で3台必要です。他にもマネージドなDBで全文検索するにもプラグインや拡張が自由に入れられず日本語の検索に向いたトークナイザが使えなかったりと、安価で楽に運用できる方法はあまりありません。
そこで fly.io の LiteFS Cloud なら10GBまでは $5/月/組織です。fly.io の請求は月 $5未満? *1*2 は $0 になるようなので実質無料(?)*3で冗長構成です 。あとはいい感じに全文検索ができるようになれば解決です。Litestream も良さそうですね。
SQLite のカスタムトークナイザ
SQLite には SQLite FTS5 Extension という全文検索用の Extension があり、これを使うとSQLで全文検索できます。
CREATE VIRTUAL TABLE email USING fts5(sender, title, body); SELECT * FROM email WHERE email MATCH ? ORDER BY bm25(fts);
デフォルトではあまり日本語の検索に向いたトークナイザはありませんがカスタムトークナイザを load して使うことができます。
既存の日本語向けのカスタムトークナイザは手元でうまく動かせなかったりしたので、signalapp/Signal-FTS5-Extension を fork してみました。オリジナルでは Unicode Text Segmentation を使ってますが、日本語を検索したいので lindera-morphology/lindera を使うように書き換えていきます。Lindera は Meilisearch などでも使われている Rust で書かれた形態素解析ライブラリです。動詞の活用や漢数字の正規化もやってくれます。
できたら .load
してテーブル作成時にtokenizerを指定して使います
.load "./lib/libsignal_tokenizer" "signal_fts5_tokenizer_init" CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize="signal_tokenizer");
signalapp/Signal-FTS5-Extension のライセンスは AGPL 3.0 なので忘れず公開しましょう。
https://github.com/mechairoi/Signal-FTS5-Extension
デモ
試しに簡単なWebアプリケーションを書いて、fly.ioの一番小さいマシン(shared 1 cpu, 256MB)にデプロイしてみます。
https://wispy-wildflower-1272.fly.dev/
アクセスが無いときは0台にスケールする設定にしているのでコールドスタートは遅いです。 辞書は lindera-ipadic、データセットは日本語 Wikipedia のサブセット(134万件、335MB) を使っていて、DBのファイルサイズは1021 MBになりました。 アプリケーションのソースコードはこちら
https://github.com/mechairoi/litefs-fts-demo
マッチする記事数が多くなるクエリでは少し遅くなりますが、インクリメンタルサーチでも違和感なく使える速度が出ていると思います。
この記事は はてなエンジニア Advent Calendar 2023 3日目の記事でした。 次は id:mizdra さんです。楽しみですね。
*1:https://community.fly.io/t/questions-on-100-billing-discount/11133
*2:先月はダッシュボードでは $5.06 でしたが 100% Discount でした
*3:無料で使うにはインスタンスのストレージがトータル3GBまで、詳しくは https://fly.io/docs/about/pricing/