はじめまして。Wantedly Visitの推薦基盤チームの一條です。普段はData EngineerやMLOpsなどに取り組んでいます。
Wantedlyではユーザーと企業のマッチングをより良いものにするために募集検索やスカウトでのユーザーの検索を日々改善しています。
しかし、改善が進むにつれて様々な問題が発生してきたので、その問題についてやその対応にどういった取り組みをしたかについて書いていこうと思います。
この記事では2年ほど前に僕が取り組んだ内容について書いています。
改善のフローへの課題意識
Wantedlyの検索周りは基本的にデータサイエンティスト(以下DS)が改善しています。3年ほど前はDSの改善をリリースするためには、自らWantedly Visit全体のコードが入ったレポジトリ(ここではvisit_allと呼ぶことにする)内のコードを変更するフローになっていました。
しかし、このフローによる問題がいくつかありました。
1. 検索の再現性
当時、visit_allから様々なロジックやデータを使い検索を行っていたため、なにかが変わると検索結果が変わってしまい、問題が起きた際の深堀りが非常に困難でした。例えば検索で利用している関数が他の箇所でも使われていてその改善で影響を受けたり、利用しているデータの保存場所がElasticsearchやRDBなどに分散しており、追っていくのが大変でした。
これを解決するために、
- 検索をする際の入力(会社、ユーザー、フィルタリング項目など)
- 検索される側のデータ(募集であれば募集のデータ、スカウトであればユーザーのデータなど)
この2つを明確化する必要がありました。
2. 信頼性
検索はWantedly Visitにおいて、プラットフォーム全体の成長を担う重要な機能です。なにかの拍子に検索が壊れてしまったり、逆に検索で扱うデータサイズが大きくなることにより、他の箇所に影響を及ぼす、もしくはサービス全体を落としてしまう、ということが起きます。
これを解決するために、ミドルウェアレベルの分離や、コード変更の影響範囲の局所化、検索に関する網羅的なテストが必要になりました(網羅的なテストをどう書きやすくしたかについては別記事で書きます)。
3. メンテナンス性
visit_allにコードが入っていることにより、気づいたら思わぬ箇所で思わぬ使い方がされており、コードのメンテナンス負荷が高まる、手を入れる際には検索を改善したいだけなのに、コードジャンプしていくと結局Wantedly Visitの様々なコードを読む必要が出てくる、などが問題になっていました。
4. 人間の並列性
基本的に全フローをデータサイエンティスト1人が行うため、次が問題になっていました。
- リリースまでにリリース用のコードを書くという、本来並列化できる箇所が直列になってしまっている
- 1つのプロジェクトに掛けられる期間はある程度決まっており、そのためリリースのフローを改善するための時間を割きづらい
- アルゴリズム改善のために検索に関する複雑なコードの大部分を把握する必要があり、新しいDSのオンボーディングコストが高い
この3つの問題を解決するために、データサイエンティストの変更のデプロイのフローを全て担当する人(もしくはツール)の必要性が出てきました。
アーキテクチャの刷新
マイクロサービスへの分離
1. 検索の再現性
2. 信頼性
3. メンテナンス性
この3つの解決策として、検索を行うサービスをコードレベルで分離し、検索を行うためのサービスを新たに作成することにしました。
これは適切に分離することでコードの読む範囲や、それらの入力・出力を明確化すること、そしてコードやミドルウェアレベルで分離することで影響範囲をわかりやすくするというのが目的です。
4. 人間の並列性
この部分に関してはマイクロサービスでは改善できないため、初期は専属の人を用意し、その後はコードの自動生成で解決することにしました(詳しくは別の記事で説明します)。ただ、1~3が解決されることにより、取り組みやすくなりました。
RubyからGoへの書き換え
また、言語としてはRubyだったものをGoに書き換えました。言語レベルで変更した理由としては、将来的な検索への改善策を考えたときに有益だったからです。
具体例としては、
- 並列・並行なコードが書きやすい。これは実際Rubyで検索部分を書いていて、かなり並列・並行なコードを書くことで速度面での改善や、利用する機械学習モデルの選択肢を増やすなどの選択肢が実装コスト的に取りづらかったためです
- 型があることにより、型があれば未然に防げた障害もあった
- 検索の再現性を解決するために、gRPCやProtocol Buffersを使いたく、(おそらく)Goが一番そのあたりの情報が多かった
などの面で優位だと判断したからです。
全体的な構成
基本的に検索部分については検索をする際の入力に関してはgRPC + grpc-gateway、検索される側のデータはCloud Pub/Sub + Protocol Buffersを利用してインターフェースの分離を行いました。
全体像としては次のようになります。
before
after
実際切り出してどうなったか
チームの並列性や専門性が分離され、推薦基盤チームができたことにより、
- DSメンバーが新しく入ってもある程度インターフェースが切れているため、オンボーディングコストがかなり減り、またリリースまでの速度感は大きく変わった
- 中長期的な改善を専任に行う、例えば今後半年でほぼ確実にやりたくなる施策に必要な改善を先に行う
といったことができるようになりました。
またサービスとして分割できたことにより、検索の再現性、信頼性、メンテナンス性の全てが解決でき、更に1年ほど前のスカウトのリニューアルに伴い、コードのリニューアルも行っていましたがその影響範囲が見えやすくなり、またロジックもインターフェースが明確になっていることにより再利用性がぐっと高まっていました。
余談ですが、初手でいきなりフルスクラッチで書き始めてしまったので、インターフェースの分離やミドルウェアの分離は同じレポジトリ内で先にやってもよかったかもしれない、という反省があります。
また、フルスクラッチで書き直した事により複雑な検索可能な条件、例えばスカウトでユーザー側がある会社を非表示に設定している場合、その会社の検索結果には出さないなど、が明確になりドキュメント化もできたので、その後にも活かせたのではないかと思います。