DMM.comの、一番深くておもしろいトコロ。

検索改善を支える A/B testing Infrastructure アーキテクチャの概要

検索改善を支える A/B testing Infrastructure アーキテクチャの概要

  • このエントリーをはてなブックマークに追加

はじめに

こんにちは、データサイエンスグループの新田です。
基盤エンジニアというロールで弊グループのバックエンド・インフラの開発・運用に従事しています。

データサイエンスグループでは、DMM サービスにおいて、ユーザの検索体験の向上させるために、
A/Bテストを用いた効果検証を通して 検索改善 に取り組んでいます。

組織・システム等の制約がある中で、どのようなシステムアーキテクチャを構成し、
検索改善施策の適用や A/B テストを実現しているのか。この記事で事例を紹介したいと思います。

施策内容の詳細は、一緒に仕事をしているデータサイエンティストやMLエンジニアの方が別の記事で紹介をする予定ですので、ここでは割愛させていただきます。

従来のDMMの検索アーキテクチャ

DMMでは50を超えるサービスを提供しており、様々なデジタルコンテンツを取り扱っています。
それらのサービスの大半にはデジタルコンテンツを検索できる機能が備わっています。
この検索機能は専門の 検索アプリケーショングループ が検索エンジンOSS Solr を用いた検索システム基盤を構築・運用し、
検索するためのAPI search-api 、インデクシングするためのAPI indexing-api をサービスに提供しています。

(図中はユーザトラフィックのみにフォーカスし、コンポーネントを単純化しています。)

この検索システム基盤は接続しているサービスから全てのリクエストを受けています。
DMMの検索を支えているシステムの信頼性やユーザビリティ向上の取り組みは 検索アプリケーショングループが取り組んでくれています。

一方で、検索体験に データサイエンスに基づいた付加価値 を与えて事業のグロースに繋げるのが我々データサイエンスグループのミッションです。

データサイエンスグループでは、
検索体験を向上するために機械学習などの技術を用いた施策を適用しています。
また、施策の効果検証のための A/B テストを回すデータドリブンな文化があります。

例えば、
「機械学習技術 X を応用してユーザの嗜好を分類し、この情報を検索クエリに付与すれば、
ユーザ毎にパーソナライズされた検索結果を返して体験向上に繋がるんじゃないか?」 という仮説を考えたとします。

  • 検索クエリに何も変更を加えないコントロール群 A
  • 検索クエリにユーザの嗜好を付与する介入群 B

の2つのトラフィックコントロールを実施し、 A/B テストで効果検証を行い、
最終的に A か B どちらを適用していくかの意思決定をしていきます。

そのため、素早く安全に施策のサイクルを回していきたいのですが、
多くのDMMのサービスに利用されている検索システム基盤は歴史が長く、直接的に手を入れることは難しい状態でした。

このような状況下で、我々がどのようなアーキテクチャのアプローチで
検索改善、つまり検索機能の付加価値を上げる取り組みをしているかを掘り下げていきたいと思います。

プロキシアプローチの採用

データサイエンスグループでは共通のシステム基盤 (ai platform と表記します) を構築・運用しています。
ai platform は、 Kubernetes クラスタベースのマイクロサービス基盤です。 今回説明する検索プロキシ以外にレコメンドやキャンペーンなどのプロダクト系のサービスや、
それらが共通で呼び出す A/B テスト管理のサービスなどを展開しています。

我々はこの ai platform 上の資産と統合した形での検索改善の体制を構築しています。

検索システム基盤(の search-api) の前に、さらに API サービスを立て、
改善施策の対象である事業部サービスにはこちらのAPIにリクエストをしてもらうように段階的に導入しています。
このAPIを 検索プロキシAPI といいます。

検索プロキシAPIは、ユーザリクエストの検索クエリを書き換えて search-api に転送したり、
search-api からのレスポンスの検索結果を書き換えて事業部サービスにレスポンスを返したりします。

つまり 検索プロキシAPI のリクエスト・レスポンス仕様は search-api のものと完全に互換性があります
互換性があるようにしている理由は以下の通りです。

  • クライアントである事業サービスにかかる工数を最小にするため
  • (検索改善は PoC 要素が強く、 ROI が悪い可能性があるため)撤退しやすいように

また、 ai platform 上に展開したのは以下のような理由があります。

  • ai platform の A/B テスト管理サービスとインテグレーションするため
  • ai platform のその他マイクロサービスや改善施策用のデータ連携をするため
  • 検索システム基盤への影響を最小限に抑えて検索改善のリリースサイクルを独立させるため
  • 歴史の長い検索システム基盤とのインテグレーションにいろいろと制約があったため

例えば、
リクエスト時点でユーザの嗜好を表す属性情報を DynamoDB から取得して、
検索クエリのソートの要求をユーザごとに変える(Solr の function query などを用いる)ことで
そのユーザの嗜好に沿った商品を検索結果のより上位に表示させることができます。

また、レスポンス時点でユーザの購買済みの商品が検索結果に含まれている場合、その商品は並びの後ろの方にずらすようにすることもできます。

名前の通り、検索プロキシAPIは search-api の前面に立ち、一部または全てのリクエスト・レスポンスに対し変更を行うことで、
弊グループの検索改善を実際にユーザに適用する役目を持っています。

(他にも最近取り組んでいることとしては、ユーザと検索結果の商品のベクトル表現を取得してそれらの距離計算をもとにリランキングを行う施策などもこのアーキテクチャで検証しています。)

「一部のユーザリクエスト・レスポンス」と表現したように、
A/B テストを用いて一部のユーザのみに新しい施策を適用するようにコントロールする必要があります。
次のセクション以降は、このA/Bテストの要件をどのように実現しているかについて説明していきたいと思います。

A/B テストの機能概要とインタフェース

全ての施策のリリースは A/B テストで効果検証をしています。
そのためには、先ほど挙げたようにユーザ群を複数の実験群に割り当てる仕組みが必要になります。

この A/B テストの適用には、 ab-api というサービスを共通で利用しています。

ab-api は 実験群の割り当て (Variant Control) を提供します。
ユーザ識別子と実験名を入力して擬似ランダムハッシュ関数を通して実験群を示す番号である A/B Group ID を出力します。

つまり、 f(ユーザ識別子, 実験名) です。
f はユーザ識別子と実験名を連結した文字列からハッシュ値 (hex) をとり、そのハッシュ値を十進数に変換したものを mod 100 します。
よって f の値域は [0, 100) の範囲をとります。

そして、

  • 値が [0, 50) の範囲であれば、 A/B Group ID は 1
  • 値が [50, 100) の範囲であれば、 A/B Group ID は 2

といったように分類します。

この関数 f は ab-api 環境に限らず、弊社のビッグデータ基盤上のSQLでも再現可能な関数でデザインされています。
これによって、A/Bテストの効果検証は ab-api の実装に依ることなく行うことができています。

ab-api のコンフィグレーションにて、実験群の割り当ての割合や、適用タイミング(開始時刻・終了時刻)の設定が行えます。
例えば、 10月1日の 15:00 ~ 10月15日の 14:59 の二週間の A/B テストを実施する際のコンフィグレーションはこんな感じです。

- ab_test_name: 実験Foo
  test_conditions:
    - start: 2022-10-01T15:00:00Z
      end: 2022-10-15T14:59:59Z
      ab_test_groups:
        - group_id: 1
          lower_bound: 0
          upper_bound: 50
        - group_id: 3
          lower_bound: 50
          upper_bound: 100

ab-api はこの YAML ファイルのコンフィグレーションを読み込み、
期間内に 実験Foo の A/B Group ID 割り当てリクエストがくれば、関数 f を通して、
[0, 50) の範囲ならば 1, [50, 100) の範囲ならば 3 を割り当てます。

あとは呼び出し元のサービス側で、
A/B Group ID 1 をコントロール群(施策を適用しない)、 A/B Group ID 3 を介入群(施策を適用する)といったように扱います。

このようにして A/B テストによるロールアウトを制御しています。
(ちなみに、デバッグ用に、任意の識別子に対しては固定の A/B Group ID を返すように設定できるような機能も提供しています。)

簡単に ab-api について紹介しました。このサービスは ai platform にデプロイされており、
検索プロキシAPI以外のサービス (レコメンドのAPIとか) でも使われています。つまり、弊グループのオンライン実験を支える汎用的なサービスです。

施策ごとのコンフィグレーションを実現するインタフェース

先述したように、検索プロキシAPIは検索リクエスト・レスポンスをインターセプトして下記のようないろいろなテコ入れを行います。

  • 検索クエリにユーザの嗜好を付与することで検索結果をブースト
  • 検索結果の商品のリストを更にユーザごとに並び替え
  • 検索結果のファセットリストをユーザごとに並び替え
  • そのユーザの購買済みの商品の位置を後ろに調整

これらの実行処理の基本単位を Experiment というコンセプトとしてモデリングしています。

  • 検索クエリにユーザの嗜好を付与することで検索結果をブースト をする Experiment
  • 検索結果の商品のリストを更にユーザごとに並び替え をする Experiment
  • 検索結果のファセットリストをユーザごとに並び替え をする Experiment
  • そのユーザの購買済みの商品の位置を後ろに調整 をする Experiment

加えて、 ab-api と連携してきめ細やかな A/B テストコントロールができます。

また、検索プロキシAPIは、既存の Experiment を変更することなく、
新しい Experiment を多段階で適用できる Experiment Chain メカニズムと呼んでいる複数の Experiment の適用をサポートしています。

このメカニズムは、単一の Experiment で多くの機能を実装するのではなく、
少数の機能のみを持つ小さな Experiment を組み合わせることによって、柔軟に多様性を実現できると考えています。

Experiment の組み合わせは、検索プロキシAPIが起動時に読み込む yaml フォーマットのコンフィグレーションファイルに記述します。
このコンフィグレーションは A/B Group ID に対応する Experiment を指定できるインタフェースになっています。

...
- ab_test_name: 実験Foo
  routes:
    - ab_group_id: 1
      experiment:
        # Experiment A
        typed_config:
          "@type": <type url>
          ...
    - ab_group_id: 2
      experiment:
        # Experiment B
        typed_config:
          "@type": <type url>
          ...
- ab_test_name: 実験Bar
  routes:
    - ab_group_id: 1
      experiment:
        # Experiment C
        typed_config:
          "@type": <type url>
          ...
    - ab_group_id: 2
      experiment:
        # Experiment D
        typed_config:
          "@type": <type url>
          ...
...

このコンフィグレーションの Specification は ProtocolBuffers で定義しており、
Go プログラムへのバインドするコードを protoc-gen-go プラグイン、
バリデーションロジックのコードを protoc-gen-validate プラグインを使って自動生成しています。

(余談ですがこの辺りのコンフィグレーションのデザインは、普段から ai platform で活用しているミドルウェアOSSの Envoy Proxy からインスパイアを受けています。)

Experiment の設定(routes[].experiment.typed_config 以下)は google.protobuf.Any で任意の型を受け付けるようにしています。

experiment:
  typed_config: {<google.protobuf.Any>}

"@type" フィールドには任意の Proto Message の Type URL(type.googleapis.com/<package>.<message>)を指定することで、
検索プロキシAPIアプリケーション側でこの Type URL を用いて該当する Experiment を解決し、 Experiment のファクトリ周りの処理を実行しています。

この仕組みによって同じフォーマット上で Experiment ごとに異なる設定を可能にしています。

- ab_test_name: 実験Baz
  routes:
    - ab_group_id: 2
      experiment:
        typed_config:
          "@type": type.googleapis.com/<experiment X>.Config
          repository:
            dynamo_db_table_name: ...
          adding_solr_parameter_set:
            bf: ...
            tie: ...
    - ab_group_id: 3
      experiment:
        typed_config:
          "@type": type.googleapis.com/<experiment Y>.Config
          repository:
            purchase_status_service_host: ...

検索プロキシAPIは、起動時にこのコンフィグレーションを読み取り、
記述された Experiment のコンフィグレーションをもとに Experiment のファクトリを呼び出しインスタンスを生成します。

ユーザリクエストのタイミングで各実験の A/B Group ID を ab-api から取得し、
それぞれの A/B GroupID に対応した Experiment インスタンスをピックアップし、
ユーザごとの experiment chain を構築します。

リクエストパスは experiment chain を正順に走査して experiments を適用し、
アップストリームの search-api にリクエストしたあと、
続いてレスポンスパスでは experiment chain を逆順に走査し experiments を適用していきます。

リクエストパスでは Experiment の OnRequest() メソッドを呼び出し処理を実行し、
レスポンスパスでは Experiment の OnResponse() メソッドを呼び出し処理を実行するように設計しており、
各 Experiment の実装はこれらのメソッドを実装することでインタフェースを満たします。

コンフィグレーションファイルとして外部から設定可能にしておくことで、実験をセルフサービス化し、
施策実行者であるデータサイエンティスト/MLエンジニア自身で実験の変更をプロダクションに適用する体制ができています。

プロキシアプローチの評価

上述したように、我々は検索改善の取り組みのためにプロキシアプローチのアーキテクチャを構成しました。

得られたメリットとしては、
検索システム基盤の間にプロキシとしてAPIサービスを配置し、
検索システム基盤には直接手を加えずともこの検索プロキシAPIを起点とした検索改善のリリースができるためです。

検索システム基盤に手を入れる形で、我々が取り組みたい検索改善の要件を実現しようと思うと、
多くの課題点が浮かび上がりサービスインまでの工数が膨れ上がってしまうのですが、
「そもそも データサイエンスを用いた検索体験の改善は有用なのか?」 という不確実性の高い取り組みであることも考慮すると
今回のプロキシアプローチアーキテクチャの採用は、検索改善の有用性を最短で確認できる良い選択だったと思います。

デメリットとしては、いろんな意味のオーバーヘッドが生じていることです。
パフォーマンス観点ではいくつものサービスが間に挟まることによるレイテンシの増加などがあります。
組織コミュニケーション観点でも、複数のチームや部署が絡んでいることによるオーバーヘッドは最適化の余地があります。

また、あくまでも検索のリクエスト・レスポンスを書き換えることしかできないので、
形態素解析器を変更したいとか、ユーザ辞書に単語を追加したいとか、
Solr の Analyzer レベルに手を入れる必要のある変更には対応できません。
その点については以下の理由があります。

  • 上記の改善施策の方が現在追っているビジネスKPIに対してのインパクトが大きく相対的に Analyzer を利用するケースの優先度が低い
  • Analyzer に手を入れるには現状の検索インデックスの設計を抜本的に見直す必要があるため工数が膨れ上がってしまう

これらの理由を鑑みて、上記アーキテクチャの制約を許容する判断をしています。
このKPI設定の考え方については先日公開したこちらの記事で紹介しております。

https://inside.dmm.com/entry/2022/4/7/engineer-search

現時点では、検索改善自体がDMMにおいても始まったばかりの取り組みのため、
今後は中長期的な観点でアーキテクチャ・組織をリデザインしていく必要もあるかもしれません。

おわりに

検索改善を実現する基盤のアーキテクチャについて紹介させていただきました。
弊グループではこの仕組みに乗って日々データサイエンスを活用した検索体験の改善にトライしています。

ご覧のように仕組みとしてはシンプルなアーキテクチャではありますが、
1年ほど前に検索改善を専門とするチームを立ち上げ、何度も施策を打ってきたことで、年間目標を大きく上回る成果を出せています。
今後は施策ロジックに高度な技術を組み込んで、検索体験の質を高めることにより注力していく予定です。
これまで取り組んだ検索体験の改善事例の一部をこちらの記事で紹介しています。

inside.dmm.com

再掲になりますが、我々が取り組む検索改善の考え方をまとめたイントロダクションの立ち位置の記事はこちらです。

inside.dmm.com

今後も公開できるような改善事例・それを支える基盤事例はどんどん公開していきたいと思います!

また、これらの検索改善など弊グループの施策を支えるプラットフォームを構築するポジションの採用をオープンしています。
より高度な機械学習の導入等を段階的に進めているのもあり、より一層、デリバリーの仕組みが重要になってきています。
そのためにはOSSやマネージドサービスを組み合わせつつも我々に合った抽象度の基盤を実現するために内製していく必要もでてきます。
そんな目標に向けて一緒に開発してくれる仲間を募集しています!

https://dmm-corp.com/recruit/engineer/1018/

もちろん改善自体を行うMLエンジニア・データサイエンティストのポジションもご興味ある方は是非こちらのポジションもよろしくお願いします!

https://dmm-corp.com/recruit/engineer/232/