最近のブログ投稿で、Mediumは「Rex」という名前の自作のGoベースのレコメンデーションサービスをどのように構築したかについて説明している。
そのサービスの一部として、Mediumは各ユーザに推奨ストーリーのパーソナライズされたリストを表示する(「ランク付けされたフィード」と呼ばれる)。元の推奨サービスはNode.jsの一部であり、150ストーリーしかランク付けできなかった。しかし、Mediumは、このサービスが1秒未満でユーザあたり数十万のストーリーをランク付けしたかった。そこで、Goを使用してまったく新しい別個のサービスを構築することにした。
Mediumの元ソフトウェアエンジニアであるMiles Hinson氏は、この選択の背景にある理由を説明している。
2018年3月にレコメンデーションシステムに最初の小さな調整を加えたとき、それぞれの変更に対するテストと検証に、望んでいたよりもはるかに時間がかかることがわかりました。テストで見つけて再現するのが難しいバグが現れます。より迅速に新しいレコメンデーション戦略に移行してテストしたい場合は、少なくとも既存のコードの大幅なリファクタリングが必要になることは明らかでした。
Hinsonによると、チームはMediumで普及しているプログラミングプラットフォームであるNode.jsを手放すことにした。その理由は、Node.jsがシングルスレッドであり、非同期I/O操作を調整する単純な計算タスク用に最適化されているためである。ただし、フィードのランク付けは、CPUをかなりの時間占有し、他の要求をブロックする可能性のある計算上の手間のかかる作業が必要である。この制限により、各リクエストの遅延が増加するが、これは次善である。
出典: https://medium.engineering/rex-mediums-go-recommendation-microservice-e077bc9582a
彼らはいくつかの理由でGoを使うことにした。まず、Goはマルチスレッドであり、作業を個別のGoroutineに分割し、待ち時間を短縮できる。第二に、Goは、初心者でも簡単に習得できると考えられている。また、MediumはGoの使用経験がある程度あるため、言語に関する知識はすでに社内にあった。
Mediumのレコメンデーションフィードは、次の図に示すいくつかの基本的なステップで構成されている。各ステップは拡張可能であり、新しい要件が発生したときに追加のルールをプラグインできる。
出典: https://medium.engineering/rex-mediums-go-recommendation-microservice-e077bc9582a
- 集約 - さまざまなプロバイダーからユーザに関連するストーリーを収集する:ユーザがフォローする出版物と著者、ユーザがフォローするトピック、ユーザの読書履歴に基づくレコメンデーションなど。プロバイダーのコレクションは時間の経過とともに拡大し、Mediumは将来、それらを簡単に変更する必要がある。
- 前処理 - 所定のタイミングでユーザに適さない可能性のあるストーリーを除外する。たとえば、ユーザがすでに読んだストーリー。アグリゲーションプロバイダーと同様に、前処理ルールは固定ではなく、時間の経過とともに変化する。
- アノテーション - さまざまなMediumのデータストアからのランク付けするために必要なデータを追加して、考えられるストーリーを充実させる。ほとんどの機能はオフラインのScalaジョブを介して計算され、フィードの作成中に読み込まれる。ただし、一部のデータはオンラインで確認する必要がある。
- ランキング - アノテーションステップの結果を数値の配列に変換する。そして、各ストーリーと値のセットを、フィードランキングモデルをホストする別のMediumマイクロサービスに渡す。この個別のマイクロサービスは、各ストーリーにスコアを割り当てる。
- 後処理 - ランキングモデルからの生の出力よりも優れたユーザエクスペリエンスを保証する。たとえば、ランク付けされたリストのソースを多様化するビジネスルール(「単一のエンティティがユーザのフィードのトップを支配するのを防ぐ」)。
- キャッシング - ランク付けされたフィードをRedisに一定期間保存する。
- 検証 - キャッシュされたフィードに追加のビジネスルールを適用する。そして、フィード内の「古い」エントリを無効にする。たとえば、その間にユーザがフォローを解除した著者のストーリーを削除する。