10X の検索を 10x したい

いやー、まいったね。

入社して三ヶ月が経ちました @metalunk です。この三ヶ月は検索インフラの改善に取り組み、検索速度 10x, インフラコスト 80% 減の成果が出ました。この記事では検索インフラ改善でやったことを説明します。

ところで、検索インフラの改善ができるということは、先人たちが検索機能を作り、PMF してサービスが利用されるようになったおかげです。感謝して改善しましょう。

2021年12月の Stailer の検索

10X は開発不要でネットスーパーアプリを立ち上げられるシステムである Stailer を開発しております。Stailer での購入のうち 35% が検索経由で行われており、検索はとても重要な機能です。

しかし、2021年12月、増加するリクエストによるサーバー負荷の増大、速度の低下に悩まされておりました。一時的にサーバーを増やし、スケールアウトをすることで対処をしていました。

問題の調査

Stailer の検索は Elasticsearch を使って実装されており、インフラに Elastic Cloud を利用しています。インフラ Metrics を Grafana で分析し、次の問題点がわかりました。

  • ピークタイムの CPU usage が高いこと
  • 平均 Response time が遅いこと

Response time は 95 percentile よりも average の方が大きいことから、ボトルネックになっているクエリがあると予想しました。

ボトルネックのクエリを発見するために Slow log を出力しました。Elasticsearch で Slow log を出力するには、warn, info, debug, trace それぞれの閾値を設定し、さらにどの閾値までログを出力するかを設定します。 https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-slowlog.html

次に、Elastic Cloud の画面をポチポチし、分析用 Elasticsearch にログを流す設定をします。これで分析用の Elasticsearch を向いている Kibana からログ解析ができるようになりました。

調査の結果、ボトルネックになっていそうなクエリを発見し、Kibana の Dev Tools の Search Profiler を使ってクエリの解析をしました。

Search Profiler ではこんな感じに、内部的にどんな Lucene クエリが発行され、どれくらい時間がかかっているのか調査できます。モザイクを不必要に多めにしているのは、隠されると見たくなる人間の真理を利用し、カジュアル面談に誘導するためです。Twitter で声かけてください。

long -> keyword で検索速度 10x

Elasticsearch には mapping という機能があり、 field ごとに type を指定できます。type は document から自動で推測してもらうこともできますが、Stailer では mapping を明示的に指定してあります。

問題のクエリを Search Profiler で眺めていると、ある long 型の field による絞り込みで Lucene の PointInSetQuery を発行しており、とても遅いことに気づきました。詳しい Lucene の実装を知らないので想像になりますが、long 型の field を検索したかったら木構造を使うとして O(logN) ほどかかるでしょう。

そこで、サーバーサイドの実装を読んでその field の使われ方を見てみると、enum 的な使われ方をしていることがわかりました。enum のようなデータは keyword 型にすることで、 Inverted index が生成され、検索時に O(1) で絞り込めます。計算量で大きな改善が見込めます。

実験環境で type を変更して Search Profiler で確認すると、クエリ単体で 283x 高速化することがわかりました。リリースしましょう!

新しい mapping を作り、適用のために Reindex を実行し、デプロイした結果、検索速度が 10x しました👏

さらに、CPU usage も 68% 減少しました👏

ちなみに、10x と書いたときは10倍のことで、10X と書いたときは会社のことなんです。

Deployment 引っ越しで費用を 80% 削減

さて、サーバー負荷と Response time には平穏が訪れましたが、スケールアウトしたままなので、お金はどんどん消えていきます。

Stailer の Elasticsearch の Instance type は I/O optimized という、大きな Storage を抱えたものを使っていました。しかし、Stailer の特性は、データ容量がそれほど多くなく、検索が多い、CPU heavy なアプリケーションです。CPU optimized instance に切り替えた場合の試算をしてみると、費用を 80% 削減できることがわかりました。直近の支払い金額が削減できるだけでなく、将来的にお客さまが増え、スケールアウトするときにも安く済みます。

わかりやすく例えると、毎日100個以上の小包をできるだけ速く配送したいんだけれど、これまではたくさんのハイエースを買って並列に配送していました。これからは少ない台数の速いバイクで配送します。バイク便です。

しかし、Instance type は途中で切り替えることができないため、新たに CPU optimized を設定した Deployment を作って引っ越す必要があります。さらに、データを失わず、無停止で引っ越しを完了したいです。

引っ越し計画の概要はこうです。

  1. Elasticsearch の更新をすべて PubSub 経由にする
  2. 新しい Deployment を作り index をコピーする
  3. Double write (両 Deployment への書き込み) 開始
  4. 新しい Deployment に参照を切り替える
  5. Double write 停止
  6. 古い Deployment を削除する

詳しく説明します。

1. Deployment の更新をすべて PubSub 経由にする

これまでは Firestore の元データに変更があったら、Cloud Firestore triggers から直接 Elasticsearch を更新していました。

それをこう変更します。Firestore の元データに更新があったら Cloud Firestore triggers が PubSub topic に publish する。それを worker が subscribe して Elasticsearch を更新する。

間に PubSub を挟むことで、複数の Subscription を生やして Double write ができるようになりました。さらに、耐障害性も向上します。

2. 新しい Deployment を作り index をコピーする

当初は、Elastic Cloud が提供している Restore from snapshot 機能を使って index をコピーしようとしましたが、動きませんでした。理由はいまだ不明で、現在も Elastic 社に調査してもらっています。

そういうわけで、代わりに Reindex from remote 機能を使ったツールを作ることにしました。ツールが行うことは、新しい Deployment に index を作り、mapping 等の設定をし、稼働中の Deployment から Reindex を実行することです。

3. Double write 開始

2つめの Subscription を生やし、それを subscribe して新しい Elasticsearch を更新する2つめの worker を動かせばふたつの Deployment を更新し続けられます。

あとは、安心なタイミングで参照を切り替え、後片付けをするだけです。

結果

無事故で作業が完了し、新しい Deployment の CPU optimized instance で検索機能を提供できる状態になりました。

これによって、費用を 80% 削減できました👏

これからやること

検索速度の改善や、インフラ負荷対策は楽しく、改善点は無くならないので永遠にできます。しかし、わたしたちの目的は、よい検索機能をお客さまに提供することであることを忘れてはなりません。

1月から3月まで取り組んだことで、インフラ負荷がひと段落し、速度も十分速い状態になり、費用も抑えることができました。

そしてついに、4月からは検索精度の改善を始めます。

CTR とゼロマッチ率を KPI とし、Dashboard を作りました。それらを改善する施策を 4, 5, 6月で実施する予定です。やっと精度改善できる状態になり、お客さま体験を改善できることにワクワクしています!

最後に

10X で Stailer の検索改善を一緒にやってくれる人を募集しております!

この記事のとおり、Stailer では検索改善を始め、やることがたくさんあります。エキサイティングな事業領域で、経験豊富でナイスなチームメンバーたちと、このサイズのスタートアップで一緒に働けることは素晴らしいです。

ソフトウェアエンジニア(検索)

そういえば、わたしは元々、機械学習で推薦機能を作るために入社したんでした。しかし、推薦の前にまずは検索、検索のためにまずはインフラということで検索インフラの改善をしておりました。

検索精度の改善に取り組み、採用もできた暁には、ついに推薦に取り組みたいと思っています。一緒に ML に取り組んでくれる人も募集中です!

ソフトウェアエンジニア(機械学習)