お知らせ!Nipoの姉妹アプリ「Maroud」がリリースされました

TypesenseとFireStoreを併用してアプリ内検索を実装【Algoliaから移住】

この記事は約11分で読めます。

AlgoliaからTypeSenseへ切替は可能か?

FireStoreを使ってシステムを構築すると、検索機能の貧弱さに頭を抱えることになります。mySQLなどにある「Like検索」は前方一致で行うことは可能ですが、部分一致による検索はできません。

FireStoreの公式サイトでは、全文検索を実装するには外部のデータベースを使用するように案内しています。2021年7月時点では、外部のデータベースとして

の3種類が案内されています。現在のNipoではAlgoliaを使って全文検索を実装していますが、実はAlgolia、小規模なプロジェクトでも結構高額になる料金体系のため予算に限りのあるプロダクトでは実装するのが非常に厳しいです。特に料金形態が変更されてからは事実上の値上げとなりました。

従量課金型なので最初は安いんだけど、あっという間に月数万円になりますよ。

使い方もかんたんで、速度も申し分ないのですが料金がネックです。
この度、Nipoではない他のプロダクトで試験的にTypeSenseを導入してみました。このページはTypesense導入におけるお話をしていきます。実際に使ってみて感じたことをつらつらと書いていきます。

TypeSenseは低価格で全文検索が可能な新しいデータベースです

Typesenseの情報自体はまだまだ少ないです。日本語によるTypesense導入を解説しているページはほとんど有りません。Typesenseがどのようなものかというと、メモリ上だけで保存される全文検索が可能なデータベースです。

制限付きAPIキーなどを使えばマルチテナント型のサービスでも安全にデータを隔離可能で、検索はもちろん、並べ替えなども可能です。

気になる料金ですが、自前でサーバを用意できる場合はそのサーバ費用のみです。サーバを用意するのが大変な場合は、Typesense Cloud(現時点でβ版)を使うこともできます。

Typesense Cloudの料金は使用するメモリ量やCPU、リージョンによって変わりますが、最小構成で、リージョンを「ムンバイ」にすると月額費用はたったの7.2ドルです。

Typesense Cloud月額費用。ムンバイは他のリージョンに比べて半額です

メモリが0.5GBは実運用では足りないと思いますが、ちょっと使ってみるにはお手軽です。多少遅くても、Algoliaよりずっと安価で導入できるのは魅力的ですね。(データ0件ならAlgoliaのほうが安いですが・・・)

リレーショナルデータベースのようにテーブル構造を作成する必要があります

Algoliaでは、Indexというまとまりを作ってそこにデータを入れて保存していきます。これはリレーショナルデータベースで言うところの「テーブル」に該当します。Typesenseではこれを「Collection」と呼びます。

ちょっと面白いなと思ったのが、テーブルスキーマという概念があることです。Collectionを作る際にスキーマを定義するため、Algoliaに比べるとちょっと面倒くさいですが、そのおかげで任意のキーでソートが可能になります。(Algoliaはソートが1種類に制限されるのでここが大きな違い)

スキーマに定義していないデータを放り込むこともできるので、ある程度の柔軟性も持っています。

例えばスキーマはこんな感じで書きます

{
  "name": "sample",
  "fields": [
    {
      "name": "id",
      "type": "string"
    },
    {
      "name": "groupId",
      "type": "string",
      "facet": true
    },
    {
      "name": "bigram",
      "type": "string",
      "facet": true
    },
    {
      "name": "createTime",
      "type": "int64",
      "facet": true
    }
  ],
  "default_sorting_field": "createTime"
}

半角スペースで単語を区切らない日本語は全文検索に工夫が必要です

もともと英語圏で作られたデータベースのためか、半角スペースで単語を区切らない日本語はTypesenseで検索することができません。そのため、日本語でも検索できるように小細工をする必要があります。この辺も、Algoliaに比べると不便な点です。Algoliaは標準で日本語の全文検索に対応していました。高いけど。

さて小細工といってもやることは単純で、n-Gramという手法を使うだけです。n-Gramは単語を決まった数にぶつ切りにすることです。ぶつ切りにする粒子の大きさによって呼び方が変わるようです。ぶつ切りサイズを2とした、「バイグラム」だと、こんな感じになります。

【ぶつ切り前】今日はいい天気です。

【ぶつ切り後】今日 日は はい いい い天 天気 気で です す。

FireStoreに書き込まれたデータを、バイグラムに変換して、Typesenseに保存しましょう。Cloud Functionで変換のプログラムを書いてあげればいいと思います。

※なぜかpreタグで上のコードを囲っても403エラーで記事が保存できないため、画像としてUpしました。タグか?タグがわるいのか?
bigram【サロゲートペア対応版】
上記のサンプルコードはサロゲートペアに対応していません。絵文字などを使う場合は、次のように少しカタチを変える必要があります

サロゲートペア問題に対応したBigram作成コード

サロゲートペアとはかんたんに言えば絵文字などUnicodeで番号の後半にある文字たちのことです。この文字列を含むデータをBigramで細切れにすると文字化けを起こすので注意が必要です。Qiitaの「文字列を1文字づつ配列化(サロゲートペアを考慮)」の記事が大変参考になります

こんな関数を用意してあげて、FireStoreのデータ変更を検知したらBigram化させ、Typesenseに書き込みします。Typesenseへの書き込みなんかは、公式サイトのデータ書き込みを御覧ください。

TypeSenseにデータを何件か保存してみた例。全部検索したいデータはBigram化しておこう

そして検索するときも、検索キーワードをバイグラムに変換して検索することで、目的のデータを引っ張ってくることが可能です。

他の会社のデータを見れないように制限付きAPIキーをうまく使おう

Algliaにもありましたが、制限付きAPIキーを作成することで、複数企業のデータがまとまったデータベースでも、他の企業から盗み見られないように安全に守ることができます。

Adminキーから制限付きAPIキーを作り、FireStoreの適当なところに保存しておきます。AdminKeyはもちろん、SearchKeyも制限がない場合は他の会社のデータを覗き見れてしまう危険な鍵なので、絶対にフロント側で使ってはいけません。Cloud Functionから本人確認のプロセスを経て、制限付きのAPIキーを作るようにします。functions.https.onCallを使えば、UserIdが本人であることが保証されるのでこれを使うと便利です。

  const limitedKey = client.keys().generateScopedSearchKey(
    dangerKey.value, // Typesenseで作成した鍵を渡します。これはそのまま使ったらあかんやつです
    {
      'filter_by': `groupId:${groupId}`, // groupIdでフィルターします。これでこの鍵は他のgroupIdデータにアクセスできません
      'expires_at': expiresAt // 有効期限なども必要に応じて設定できます
    }
  )せ

制限付きキーの作り方について詳しくはTypesense公式ガイド(API Key)を御覧ください。

ざっくばらんに説明するとfilter_by: フィルターするグループIDをセットすることで、そのグループのデータだけがフィルターされたサブセットから検索されるということです。この鍵はもうフィルターが絶対条件としてついており、利用者側でこの鍵のフィルターを外すことはできません。

制限付きKeyを使ってフィルターする。実装の考え方自体はAlgoliaとほとんど同じだね

TypeSenseのイマイチなポイント

非常に便利なTypesenseですが、イマイチなポイントもありました。いくつかまとめてみます

「文字列」のソートには対応していない

地味に不便なポイントです。数値や真偽値でのソートは可能ですが、文字列によるソートは対応していません。Nipoなんかでは正直ソートはいりませんが、データをテーブル風に表示する製品においてはテーブルとソートはセットみたいな感じなので、文字によるソートは欲しかったですね。

文字を文字コードの数値にして戦闘からウェイトをもたせて数値として保存する事もできますが、まぁそこまで回りくどいことをしないと実装できないってことで少しマイナスポイントでした。

Algoliaのソートは1種類しか保存できないため、文字によるソートができない弱点は対Algoliaに対してそこまで大きな弱点にはならないでしょう。複数のキーでソートできる点では、TypesenseはAlgoliaより優秀です。

TypeSenseの勝ちです

 

日本語など半角スペースで区切らない言語に厳しい

すでに上でも書いていますが、日本語ではn-gramを使って文字をぶつ切りにしないと全文検索として利用できません。n-gramはシンプルですが検索にノイズが入りやすく、精度がイマイチというデメリットがあります。このあたりはAlgoliaのほうが優秀ですね。なお、ハイフン区切りの英単語も検索できないとissueが上がっていました。

本記事をTypesense開発者のJason Bosco氏が見てくれて、情報を提供してくれました。
緩やかにではありますが、日本語のサポートにも着手しているとのことです。将来が楽しみですね。

現時点ではAlgoliaの勝ちです

インメモリで動くためサーバ構成を変えることができない

Typesense_cloudでは起動前にサーバの性能を選択できることは前述しましたが、一度起動すると、構成を変更することができません。

あとになって「あー、メモリが足りない」といったときに、自動でスケールしてくれればいいのですが残念ながらそれは不可能なようです。

サーバを止める(terminate)と、そのサーバは二度と起動できません。これは揮発性メモリにしかデータを保存してないためだと思われますが、何にせよ不便ですね・・・。

そのため、全文検索したいデータはFireStoreからTypesenseへ全プッシュするプログラムを書いておく必要があります。容量がやばくなってきたら新しいクラスターを立ち上げて全プッシュし、古いクラスターを破壊するといった手順が必要です。この辺をサービスを停止せずにスマートに切り替えるにはいくつか細工が必要です。この辺はAlgoliaも同じようですね。ただ、高額な料金に見合うだけの潤沢なメモリを最初に割り当ててくれるため、リソースの枯渇に悩む必要が無いってことらしいです。

本記事をTypesense開発者のJason Bosco氏が見てくれて、情報を提供してくれました。
クラスターを拡大することは、サポートに連絡することで対応してくれるそうです。また、将来的には管理画面からユーザが任意にクラスターサイズを変更できるようになるようです。

Typesenseが動的に変えられるとのことでTypesenseの勝ちになりました

ライセンスがGPL3である(用途によっては問題になるかも?)

ライセンスの問題で、GPL3を採用しています。開発者側は「なぜGPL3?」と詳しく解説しているので気になる方は目を通してみてください。

TypeScriptに対応していない

・・・タイプスクリプト化だけはお願いします。

有志の型がデコレーションファイルを共有してくれていますので、公式でTypeScriptがサポートされるまでの間に合わせとしてこちらを使っています。微妙に間違っているところもありますが概ね問題なく動きます。サポートされるまでのつなぎとして、ありがたく使わせてもらいましょう。なお、AlgoliaはしっかりTypeScriptがサポートされています。

Algoliaの勝ちです

総括

実際に使ってみて、非常に良い製品だと思います。スキーマを定義することで様々なキーでソートが可能になるため、NoSQLの弱点を少し払拭してきた印象です。SQLを書かずともそれっぽい検索が可能なデータベースであり、用途によってはAlgliaより有益な製品になるでしょう。これ以上、より細かい並べ替えが必要なら素直にRMDBを使いなさいってことだと思います。

実際に数ヶ月ほど稼働させてみて、問題がなければぜひNipoでもTypesenseを導入してみたいと思います。その際は、現在の「直近3ヶ月のデータしか検索できない」という成約は取っ払うことができるかもしれません。

 

ここまで読んでくれてありがとうございますっ
Nipoに興味を持ちましたか?すぐ始められますよ

個人情報の入力も無し。ワンクリックで体験アカウントで利用可能です。そのまま正式アカウントへ昇格もOK。Nipoが使えないと思ったらブラウザを閉じるだけです

開発者ブログ
クラウド日報 Nipo

コメント