検索エンジニアとして入社して1年でやったこと

この記事は10Xアドベントカレンダー2024の18日目の記事になります。 昨日はJOJOさん(@joj0hq)が「10Xのエンジニアとして入社から2年目を振り返る」という記事を公開しているので、そちらもぜひご覧ください。

はじめに

こんにちは、10Xで検索推薦の機能・基盤の開発運用を担当している安達(id:kotaroooo0)です。 2024年1月に入社し、もうすぐ1年が経ちます。 今回はこの1年間を振り返り、印象的だったプロジェクトをいくつか紹介します。

取り組んだプロジェクト

自分がリードしたプロジェクトをいくつか紹介します。 今回紹介する以外にもElasticCloudのオートスケーラー開発について以前書いたので参照しておきます。

product.10x.co.jp

類似商品検索の実装

商品詳細ページで、関連商品として類似商品を表示するようにしました。

商品詳細画面

Elasticsearchのmore like thisクエリで実装しました。

他の選択肢としては、ベクトル検索もありましたが工数、Elasticsearchへの負荷、技術的不確実性が高く、まずはmore like thisクエリで実装することにしました。

ただし、セマンティックな検索ができないという課題もあるため、今後改善していきたいです。

新しい検索精度モニタリングの導入

nDCGを活用してランキング精度をモニタリングできるようにしました。 これにより、既存のモニタリング項目(ゼロマッチ率、検索ごとのカート追加率、ヒット件数)に加えて、ランキングの精度も可視化できるようになりました。

これまで、検索改善は主にRecall(検索結果の網羅性)を向上させるフェーズでしたが、今後はPrecision(検索結果の精度)向上に注力していく必要があります。 これに伴い、ルールベースのリランキングやLearning to Rankを導入するためにランキング精度がモニタリングできる必要がありました。

BigQueryでキーワードごとのカート追加率を集計し関連スコアとし、nDCGを計算しました。 Looker Studioでダッシュボードとして利用し、可視化しました。

トップ画面での推薦枠A/Bテスト

推薦にも新しくチャレンジしてみました。

アプリのトップ画面に推薦枠A/Bテストを実施しました。 レジ前推薦でパーソナライズを導入し大きな効果がでたため、サービスの入り口であるトップ画面で推薦を提供することで平均注文額や平均カゴ点数の増加を期待しました。

トップ画面

アルゴリズム

ユーザーベース協調フィルタリングを採用しました。 ユーザーの購入履歴を元にユーザー間の類似度を計算しました。 ネットスーパーでは、繰り返し何度も同じ野菜、卵、牛乳等を買い続けるケースが多く、かつ注文点数が多いです。 そのため、単純に協調フィルタリングすると、どのユーザーにも人気商品を提案することになる問題がありました。

そこで、人気商品はそのユーザーの特徴量として扱わないようにしてユーザー間類似度を計算しました。 どれだけの人気商品を除外するかは、推薦結果が人気順に近づくのとユーザー間類似度の精度が落ちることのトレードオフです。 ここのパラメータはデモを実施することにより定性的な評価で決定しました。

アーキテクチャ

要件として、推論頻度を日次にしました。 ユーザーの購入履歴を元に推論結果が変化するアルゴリズムであり、ユーザーの購入は1日1回以内であることがほとんどのためです。

そのため、stailer-ml-pipelinesというVertex AI Pipelinesベースのパイプラインで日次でユーザーベース協調フィルタリングの推論をして、Firestoreに保存しておきます。 それをstailer-recommenderという推薦APIでServingします。

アーキテクチャ図

A/Bテストの結果は、ネットスーパーさんごとに異なり、他の商品枠との兼ね合いで推薦枠が効果を発揮する場合もあれば、そうでない場合もありました。 そのため、現在はプラットフォーム全体で推薦枠の表示を一律停止しています。 しかし、なぜ結果が異なるのかについて仮説を立てたり、トップ画面で特に効果的な枠について知見を得たりする良い機会となりました。

検索系RPCのレイテンシ改善

検索系RPCのレイテンシ(90パーセンタイル)をモニタリングしていましたが、基準を満たさず、アラートが頻発している状態でした。 このRPCはFirestoreとElasticsearchに複数回リクエストを送る構造になっており、どこがボトルネックなのかがすぐには特定できませんでした。

分散トレーシングを活用し、レスポンスタイムが遅いリクエストを数十件調査した結果、以下の2つが主な原因であることが分かりました。

ボトルネック

1. Firestoreへforループでのアクセス

Firestoreへのアクセスが原因で遅くなっている箇所を発見しました。 調査したところ、他チームが既に修正済みのコードが存在していたため、それを取り込むことで解決しました。 Firestoreのデータ構造を変更し、以前は複数回のクエリが必要だったものを、1クエリで取得可能な形に改善していました。

2. Elasticsearchのprefixクエリ

Elasticsearchへのクエリで、次のような空文字を指定したprefixクエリが発行されている箇所を発見しました。

{
    "prefix": {
        "hoge": ""
    }
}

この問題は、実際に生成されたクエリをKibanaのSearch Profilerでプロファイリングすることで特定しました。 期待外のクエリが発行されており、それがボトルネックになっていました。

対応策として、空文字の場合はprefixクエリを除外するように修正しました。

効果

対応の結果、対象の3つのRPCすべてでレスポンスタイムが改善し、アラートが発生しなくなりました。 それぞれの対応策リリース時にレスポンスタイムが期待以上に縮まってくれました。

レイテンシ(90パーセンタイル)

対応自体は比較的シンプルなものでしたが、、多角的な原因分析と対応策を列挙して比較したりとリアルISUCONできる貴重な機会でした。

Elasticsearchのバージョンアップ

Elasticsearch 7系から8系にメジャーバージョンアップしました。

課題

  • 7系ではElasticsearch単体ではベクトル検索できない
    • Vertex AI Matching Engineと組み合わせれば実現可能だが構成が複雑になる
      • Elasticsearchでフィルタ → Vertex AI Matching Engineでベクトル検索というフローはメンテナンス性が低い
      • Elasticsearchのみでベクトル検索ができれば、Elasticsearchのみで検索が完結し嬉しい
  • いずれ9系がリリースされるとEOLになる

互換性の調査

以下の項目について互換性を確認しました。

  • プラグイン: 形態素解析辞書や類義語辞書、ICUなど使用中のプラグインが新バージョンで動作するか確認。
  • Elasticsearchクライアント: アプリケーションが利用するクライアントライブラリの互換性をチェック。
  • 設定ファイル: mapping.json や settings.json の互換性を確認。
  • 利用機能: 非推奨(deprecated)となる機能や仕様変更をリリースノートで確認。KibanaのUpgrade Assistant機能を使ってダブルチェック。

リリース手順

Blue-Greenデプロイを採用して、本番環境でのリリースを進めました。

1. 新クラスタの作成

Elasticsearch 8系を使用した新クラスタをElasticCloudで構築。

2. 新クラスタへデータ更新導線連携

新旧クラスタに対してデータ更新をダブルライトで行い、データの一貫性を確保。 データ更新リクエストはPub/Subにキューイングされる仕組みです。 キューを処理するWorkerはKubernetes上で動いており、新クラスタ用にWorkerを新しいDeploymentとして起動し、旧クラスタ用Workerと並行して動作させれば簡単にダブルライトできるアーキテクチャになっています。

3. 検索導線の向き先切り替え

検索リクエストの向き先を旧クラスタから新クラスタに変更。 旧クラスタは障害時の切り戻しに備えて1週間程度保持。 障害が発生する可能性が高いのはここであり、切り戻し手順をリハしておきました。

4. 旧クラスタの除却

新クラスタの安定稼働を確認後、旧クラスタとそれに伴うダブルライトを除却。

バージョンアップによるパフォーマンス改善

機能面を目的にバージョンアップしましたが、非機能面でもパフォーマンスが大きく改善されました。

  • CPU利用率: バージョンアップにより半分程度に(StailerのElasticsearchはCPU bound)
  • 検索rpcのレイテンシ: 90パーセンタイルが半分程度に

これからの取り組み

LLMの活用

現在、検索機能へのLLMの活用を検討しています。 まずは、商品への自動タグ付けができないか検証しています。

例えば、「ヤッホーブルーイング よなよなエール 350ml」という商品があった時に、「クラフトビール」と検索してヒットさせたいです。 現状では、タグ付けは手動で行うか、同義語(シノニム)を登録することで対応しています。 これでは、気づかないとタグ付けやシノニム登録でできないし、追加する工数もかかります。

そこで、LLMを活用した自動タグ付けで、これらの課題を解決できないか検証しています。 GeminiやOpenAIのAPIの精度が向上しコストが低下してきているため、大量の商品でも効率的にタグ付けできる可能性が高まっています。

リランキング

これまでの Stailer の検索改善では、主に Recall を向上することに取り組んできました。 その結果、「検索結果の並び順は最適ではないかもしれないし、関係性の低い商品も含まれているが、欲しい商品は見つかる」という状態に近づけました。 次は、「関係性の低い商品は下位に、欲しい商品は上位に表示される」という状態を目指していきます。

将来的にはLearning to Rankに取り組みたいですが、まずはルールベースによる簡易的なリランキングに取り組んでいます。 これは、少ない工数でできるだけでなく、ルールベースによるリランキングをするためのプロセスがLearning to Rankにも生き、ステップを踏むことが無駄になるわけではないからです。

例えば、特徴量の分析や継続的な分析のためのオフライン/オンライン評価(デモ環境や精度モニタリング、インターリービングテスト)の仕組みなどはそのまま活用できます。

おわりに

この記事では、2024年の1年間を振り返り、自分が取り組んだ印象に残っているプロジェクトの概要を紹介しました。 これまでよくやっていた検索基盤の仕事だけでなく、推薦にも挑戦でき充実した1年でした。

明日は橋原さんの記事です!お楽しみに!