こんにちは、MA部MA開発1ブロックの齋藤(@kyoppii13)です。
ZOZOTOWNではユーザ行動に基づくキャンペーン配信を実施しています。この配信はリアルタイムマーケティングシステム(以降、RTM)と呼ばれるシステムによって実現しており、RTMでは配信トリガーや配信タイミングの最適化等にユーザの行動ログを利用しています。
この行動ログは、ユーザがZOZOTOWNのページへアクセスした際に、HTTPリクエストをRTMが直接受信する形で収集していました。しかし、RTMの既存のログ収集機能はシステム要件や運用などの課題を抱えていました。また、その一方で全社的にログを収集・蓄積する基盤も並行して運用されており、RTMはこのログ基盤を活用できていませんでした。そのため、RTMでもこの全社ログ収集基盤を利用することで既存の課題を解決しました。
本記事では、RTMにおける行動ログの活用方法と、全社ログ収集基盤への移行で考慮した点について紹介します。
RTMでの行動ログ活用方法
本章では、RTMでの行動ログ活用方法について紹介します。
まず、RTMがどのようなシステムかを説明します。RTMはユーザの行動や商品在庫の変化などをトリガーとして、ユーザごとにパーソナライズ配信をするシステムです。配信チャネルはLINE・メール・プッシュ通知があります。例えば、あるユーザがお気に入りしている商品が値下がりした場合に「あなたがお気に入りしている商品が値下がりしました」という訴求をします。
RTMで配信する場合、どのようなイベントが発生したときにどのような内容を訴求するかのルールを定義します。この定義をキャンペーンといいます。商品在庫などが変化した場合、RTMは定義されたルールに従いキャンペーン判定をします。ルールにマッチした場合、対象のユーザを抽出し、ユーザごとに配信内容(コンテンツ)を組み立てて配信をします。システム名にリアルタイムとついていますがリアルタイムな配信のみならず、配信時は最適化処理も実施し、ユーザごとに最適なチャネルや時間帯に配信をします。
イベントの検知から配信までの流れは以下のようになっています。
RTMの詳細については以下のテックブログをご参照ください。
イベント検知・各種最適化・コンテンツ生成の処理ではユーザの行動ログを利用しています。行動ログは4種類で、アクセスログ・クリックログ・メール開封ログ・コンバージョンログがあります。これらのログは、配信時の最適化、コンテンツ生成、キャンペーン判定、キャンペーン分析で利用されています。また、これらのログはブラウザやネイティブアプリなどからRTMが直接取得しています。これらのログがどのようなログなのか、どのように利用しているかについて紹介します。
アクセスログ
アクセスログはユーザのページ閲覧を表すログです。このログによって、どのユーザがどのページ(URL)にどのようなクライアント(アプリ・Web)でいつアクセスしたかが分かります。アクセスログをもとに、ユーザがアクセスしやすい時間帯や閲覧した商品などを判別します。この情報で配信を最適化し、ユーザごとに購入の可能性が高い商品情報を最適な時間に届けることができます。
クリックログ
クリックログはRTMが配信したキャンペーンをクリックしたことを検知するためのログです。このログによって、どのユーザがどのキャンペーン経由でサイトへアクセスしたかが分かります。クリックログをもとに、ユーザがクリックしやすいチャネルを判別します。そして、最適化において、ユーザがクリックしやすい最適なチャネルへキャンペーンを送信できます。クリックログを分析し、クリックしやすいキャンペーンが分かれば、ニーズに合わせたキャンペーンを考えることもできます。また、クリック回数や日時を条件にクリックの可能性が高いユーザを抽出し配信もしています。
メール開封ログ
メール開封ログによって、どのユーザがどのメールをいつ開封したのかが分かります。メールに開封ログ用の画像を埋め込むことで、開封時にこの画像が読み込まれるとRTMへリクエストされてメール開封を検知します。メール開封ログを分析し、開封しやすいメールがわかれば、開封しやすいメールの文言等をニーズに合わせて考えることができます。また、開封回数や日時を条件に開封の可能性が高いユーザを抽出し配信できます。
コンバージョンログ
コンバージョンログは他のログとは違い、直接ユーザから取得しているわけではなく、アクセスログとクリックログをもとに判定します。ユーザがどのキャンペーン経由でサイトへアクセスし、注文完了まで至ったかを検知するためのログです。あるユーザの最後のクリックログ検知から一定の時間以内に注文完了ページのアクセスログを検知した場合、クリックしたキャンペーンでのコンバージョンとみなします。このログによって、キャンペーンのCVR測ることができキャンペーンのニーズがわかります。また、コンバージョンしやすいユーザを抽出し、配信も行っています。
RTMでのログの取得フロー
ここまで紹介した各ログは以下のフローで収集していました。
ログ収集までのフローとログ到着後のフローに分けて説明します。
ログ取得までのフロー
最初にRTMからキャンペーンが配信されます。配信チャネルはLINE、メール、プッシュ通知です。ユーザがWeb・アプリ(iOS・Android)のどちらでサイトにアクセスしたかによってログ配信のフローが変わります。
まずアクセスログの場合、Webでは各ページでログ発火のためのビーコンが埋め込まれており、ページ表示時に発火しログを取得します。アプリはWebviewとネイティブのページが混在しています。Webviewの場合はWebと同様のフローです。
クリックログはアプリでプッシュ通知やディープリンクをクリックした際に発火しログを取得します。
メール開封ログは、メールを開いた際に画像ビーコンによってリクエストが送信されてログを取得します。
このように各ログはWeb、アプリ(iOS・Android)、メールから直接取得されるようになっていました。
次にRTMにログが到達した後の経路についてです。クライアントから送られるログにはユーザ情報とアクセス・クリックしたページ情報が含まれます。RTMに到着したログはまずメンバーIDをkeyとするキャッシュに保存されます。RTMはメインとなるアプリケーションがJBoss Data Grid(JDG)というインメモリな分散キャッシュデータストアを利用しており、高速な条件判定を実現しています。ログ到着時、対応するメンバーIDのキャッシュがなければキャッシュを新規作成、あれば既存のキャッシュを更新します。最適化の際にはこのキャッシュに含まれたデータを使用します。しかし、ログデータは分析などにも利用されるため配信実績としてテーブルにも保存しなければなりません。そこで、タイマーによって定期的にキャッシュデータを配信実績テーブルに書き込みます。配信実績テーブルのスキーマを以下に示します。
カラム名 | 説明 |
---|---|
id | 配信実績ID |
campaign_id | キャンペーンID |
member_id | 会員ID |
channel | 配信チャネル |
delivery_dt | 配信日時 |
open_dt | メール開封日時 |
click_dt | クリック日時 |
conversion_dt | コンバージョン日時 |
ユーザへの配信ごとに実績が記録されます。そして、RTM DBに書き込まれたログは日次のバッチ処理で全社共通のDWH(BigQuery)に連携されます。このBigQueryに連携することで、配信実績を分析用途や他システムで利用できます。
従来のログ取得における課題
従来のログ取得における課題点は以下です。
- 全社ログ収集基盤が存在しているにもかかわらずRTMでログを取得している
- 直接ログを集めている
- ログ取得時間がサーバでのログ検知日時になっている
- 配信処理とログ取得処理が密結合になっている
全社ログ収集基盤が存在しているにもかかわらずRTMでログを取得している
1つ目に全社ログ収集基盤が存在しており、このシステムが取得しているログとRTMが直接取得しているログで重複しているものがありました。全社ログ収集基盤とはZOZOTOWNで発生するログを収集してBigQueryへと連携する基盤です。RTMは全社ログ基盤ができる前からあったシステムのため、独自でログを収集し利用していました。
全社ログ収集基盤の詳細については、以下スライドとテックブログをご参照ください。
直接ログを集めている
2つ目にRTMが直接ログを取得するために外向きのAPIを提供していました。そのため、不特定多数のアクセスがあり、bot等に対する対策がRTM独自で必要でした。また、セール実施時など大量のログが来る場合にはシステムの負荷が高まります。
ログ取得時間がサーバでのログ検知日時になっている
3つ目に各ログの取得日時がRTMのサーバへ到達した日時になっていました。ログの到着が遅延して検知が遅れた場合、実際のアクセスやクリック日時と異なってしまうという課題がありました。
配信処理とログ取得処理が密結合になっている
4つ目に配信処理とログ取得が同一のシステムで動作しており、密結合になっていました。ログ取得においてはバッファレイヤーがないため、アプリケーションのメンテナンス時にはログが欠損してしまうという課題がありました。また、将来的にこの配信基盤の移行を考えているため、先にログ収集の部分を切り出しておきたいと考えていました。
これらの課題は全社ログ収集基盤へ統一することで解決できるものでした。そのため、全社ログ収集基盤のログを利用することにしました。
全社ログ収集基盤への移行
前述の課題を解決するために、全社ログ収集基盤からログを取得するようにしました。その際に考慮した点を紹介します。
移行後のアーキテクチャ
移行後のアーキテクチャは以下の様になりました。 執筆時点では移行途中であり、移行が完了したものはクリックログとコンバージョンログです。アクセスログは全社ログ収集基盤とRTMで取得しており、メール開封ログはRTMでのみ取得している状態です。全社ログ収集基盤へ完全に移行した後はRTMへのログリクエストがなくなる予定です。
2種類のログ日時
全社ログ収集基盤で集めているアクセスログやクリックログといった行動ログは、クライアントでのログ送信日時と基盤でのログ検知日時の2つが日時データとして含まれています。ログ送信日時はクライアント側で付与されるパラメータのため、ログ到着が遅延しても、ログ送信日時に影響はありません。
このような全社ログ収集基盤の仕様によって、3つ目の課題であるログ取得時間がサーバでの検知日時になっているという課題を解決できます。
ログ連携頻度と連携方法の見直し
RTMでリアルタイムに取得しているログは、アクセスログ・クリックログ・コンバージョンログの3つでした。この内、クリックログ・コンバージョンログはリアルタイムで集める必要のないことが調査の結果わかりました。そこで、バッチ処理によりクリックログ・コンバージョンログを全社ログ収集基盤から連携するようにしました。クリックログはそのまま連携すればよいものの、コンバージョンログはRTMでアクセスログとクリックログをもとに計算していたため、単純に全社ログ収集基盤から連携するだけでは実現できません。こちらについては次で詳しく説明します。
アクセス/クリックログからのコンバージョンの取得
既存のコンバージョン検知のロジックは以下です。
- 配信されたキャンペーンからサイトにアクセス。RTMがクリックログを検知し新規セッション開始。
- ユーザがサイト内を回遊しアクセスログを一定時間内に検知した場合、オンライン状態とみなしセッションを更新。
- セッションの開始/更新から一定時間内で購入ページでのアクセスログ(以降、購入ログ)を検知した場合、同一セッション内でのコンバージョンとみなす。
ログ連携頻度の見直しによって、リアルタイムでコンバージョンログは使用していないことが分かりました。したがって、RTMで判定しているコンバージョンを他で実施出来ればRTMの負荷を下げることができます。そのためこの処理はバッチ処理で実施することにしました。
コンバージョン判定のためには、アクセスログとクリックログ及び購入ログが必要です。これらのログは全社ログ収集基盤から取得できるログだったため、これらを利用しコンバージョンをバッチ処理で判定することにしました。
全社ログ収集基盤から取得できる各ログとスキーマについて説明します。
アクセスログにはどのユーザがいつどのページにアクセスしたかの情報が含まれています。アクセスログのスキーマを以下に示します。
カラム名 | 説明 |
---|---|
uid | ユーザID |
url | アクセスしたページのURL |
client_timestamp | クライアント側のログ送信日時 |
server_timestamp | 全社ログ収集基盤でのログ検知日時 |
クリックログにはどのユーザがどのキャンペーン経由でサイトにアクセスしたかが含まれています。クリックログのスキーマを以下に示します。
カラム名 | 説明 |
---|---|
uid | ユーザID |
url | クリックしたURL(キャンペーンIDをクエリパラメータに含む) |
client_timestamp | クライアント側のログ送信日時 |
server_timestamp | 全社ログ収集基盤でのログ検知日時 |
購入ログにはどのユーザがいつ購入したかが含まれています。購入ログのスキーマを以下に示します。
カラム名 | 説明 |
---|---|
uid | ユーザID |
order_id | 購入ID |
order_timestamp | 購入日時 |
これらのログにはセッションを識別するための情報であるセッションID等が含まれていません。したがって、バッチ処理ではこれらのログを利用してセッションIDを計算し、どのセッションでコンバージョンしたのかを判定します。
バッチ処理で実行されるアクセスログ、クリックログ、購入ログを利用したコンバージョン判定クエリは以下です。このクエリを実行することで、どのユーザがどのキャンペーン経由でいつコンバージョンしたのかがわかります。
WITH -- ①各ログイベントを抽出。各イベントを必要な期間抽出し、非正規化してUNIONで縦につなげる。 -- クリックイベント click_events AS ( SELECT uid, 'click' AS event, client_timestamp AS event_timestamp, REGEXP_EXTRACT(url, r'campaign_id=(\d+)') AS campaign_id, NULL as order_id, server_timestamp FROM `zozo-log-platform.event_logs.click_log` ), WHERE -- 直近3日間のデータを取得する DATETIME(server_timestamp) >= DATETIME_ADD(DATETIME'{{batch_start_timestamp}}', INTERVAL -3 DAY) AND DATETIME(server_timestamp) < DATETIME_ADD(DATETIME'{{batch_start_timestamp}}') AND REGEXP_EXTRACT(url, r'campaign_id=(\d+)') IS NOT NULL), -- アクセスイベント access_events AS ( SELECT uid, 'access' AS event, client_timestamp AS event_timestamp, CAST(NULL AS string) AS campaign_id, NULL AS order_id, server_timestamp FROM `zozo-log-platform.event_logs.access_log` ), WHERE DATETIME(server_timestamp) >= DATETIME_ADD(DATETIME'{{batch_start_timestamp}}', INTERVAL -3 DAY) AND DATETIME(server_timestamp) < DATETIME_ADD(DATETIME'{{batch_start_timestamp}}') -- 購入イベント order_events AS ( SELECT uid, 'order' AS event, order_timestamp AS event_timestamp, CAST(NULL AS string) AS campaign_id, order_id, NULL AS server_timestamp FROM `zozo-log-platform.event_logs.order_log` ), WHERE DATETIME(server_timestamp) >= DATETIME_ADD(DATETIME'{{batch_start_timestamp}}', INTERVAL -3 DAY) AND DATETIME(server_timestamp) < DATETIME_ADD(DATETIME'{{batch_start_timestamp}}') -- すべてのイベント events AS ( SELECT * FROM click_events UNION ALL SELECT * FROM access_events UNION ALL SELECT * FROM order_events ), -- ②新規セッションの判定。イベント時間を昇順にみて、1つ前のイベントと比較し、新規セッションにフラグ(session_flag=1)を立てる。 event_and_session_flag AS ( SELECT uid, event, campaign_id, order_id, event_timestamp, -- イベントをuidごとにevent_timestampごとに昇順でならべて、LAG関数を利用し一個前のevent_timestampを取得 LAG(event_timestamp) OVER (PARTITION BY uid ORDER BY event_timestamp) AS previous_event_timestamp, -- 新規セッションにフラグ立て(session_flag=1) CAST( -- 10分以上間隔が空いたアクセスは新規セッション DATETIME_DIFF(event_timestamp, IFNULL(LAG(event_timestamp) OVER (PARTITION BY uid ORDER BY event_timestamp), event_timestamp), MINUTE) > 10 OR -- クリックがあったら新規セッション event = 'click' OR -- ユーザが商品を購入してから同一セッションで商品を購入した場合はコンバージョンの対象外とするため新規セッション LAG(event) OVER (PARTITION BY uid ORDER BY event_timestamp) = 'order' AS INT ) AS session_flag FROM events ), -- ③セッションごとにユニークなIDを付与。session_flagとuidを利用しユニークなIDを付与。 session AS ( SELECT *, uid || '_' || SUM(session_flag) OVER (PARTITION BY uid ORDER BY event_timestamp, session_flag DESC ROWS UNBOUNDED PRECEDING ) AS user_session FROM event_and_session_flag ORDER BY event_and_session_flag.uid, event_and_session_flag.event_timestamp, event_and_session_flag.previous_event_timestamp ), -- ④コンバージョン判定。click_sessionとorder_sessionのuser_sessionが同じ場合コンバージョン。 -- コンバージョン判定のためにsessionからクリックイベントのみを抽出 click_session AS ( SELECT * FROM session WHERE event = 'click' ), -- コンバージョン判定のためにsessionから購入イベントのみを抽出 order_session AS ( SELECT * FROM session WHERE event = 'order' ) SELECT click_session.uid, click_session.campaign_id, order_session.order_id, order_session.event_timestamp AS conversion_at FROM click_session INNER JOIN order_session ON click_session.user_session = order_session.user_session;
このクエリの処理内容は以下です。
- 各ログイベントを抽出。
- 新規セッションの判定。
- セッションごとにユニークなIDを付与。
- コンバージョン判定。
このクエリを日次バッチ処理で実行します。このクエリについては2022年のAdvent Calendarの記事でも解説していますが、改めて各処理について解説します。
各ログイベントを抽出
最初にアクセス・クリック・購入のイベントデータが含まれるテーブルからログイベントを抽出します。イベントにはクライアント側の送信時間(client_timestamp)とログ基盤の検知時間(server_timestamp)の2種類のタイムスタンプが付与されています。この内、client_timestampをイベントの発生時間として扱います。WHERE句には取得するデータの期限を指定します。batch_start_timestampはバッチ処理時に、バッチ処理の開始時刻が設定されます。取得期間は過去1日分ではなく、数日分取得するようにしています。これは全社ログ収集基盤を利用する上で、遅延データの考慮をする必要があったためです。全社ログ収集基盤はクライアントからの行動ログ送信遅延などが原因で最大数日の遅延データが入ります。これを考慮し、バッチ実行時点から過去数日分のログを取得するようにしています。そして、各ログをUNION ALLで1つにまとめます。
新規セッションの判定
次に新規セッションの判定をします。前の処理で作成したテーブルをイベント発生時刻の昇順でみていき、1つ前のイベントと比較しながら新規セッションかを判断するフラグを立てていきます。この計算ではLAG関数を利用しています。LAG関数は前の行との比較に便利な関数です。LAG関数を用いて、直前のイベントからn分以上経っていたら新規セッションとします。また、イベントがクリックログの場合、直前のイベントが購入ログの場合はイベント間隔に限らず新規セッションとします。
セッションごとにユニークなIDを付与
次にセッションごとにユニークなIDを付与します。前の処理で計算したsession_flagとユーザごとにユニークなIDであるuidを組み合わせて、ユーザのセッションをユニークに判別できるID(user_session)を付与します。
コンバージョン判定
最後にコンバージョン判定をします。前の処理でユーザのセッションを判別するID(user_session)を付与しました。コンバージョンはどのキャンペーン経由をクリックし、購入まで至ったかを識別するものです。つまり、user_sessionが同じであるクリックイベントとコンバージョンイベントがあれば、そのクリックイベントをセッションの起点としてコンバージョンまで至ったと判断できます。そのため、クリックイベントとコンバージョンイベントをuser_sessionでJOINしています。この処理の結果、どのユーザがどのキャンペーン経由でいつコンバージョンしたかがわかります。
こうして得られたコンバージョンからコンバージョン日時をRTMのログ実績テーブルへ連携します。こうすることで、RTMで実行していたコンバージョン判定を別システムで実行できるようになりました。
今後の展望
クリックログとコンバージョンログは全社ログ収集基盤を利用してバッチでの連携をするようにしました。ただし、その他のログについては、パフォーマンステストにおいて要件を満たすことができなかったためまだ移行できていません。今後はすべてのログを全社ログ収集基盤からの連携にする予定です。
また、RTM自体をリプレイスするプロジェクトも進めています。今回述べたログの課題以外にも、施策の実施がビジネス側で完結せず、開発側に依存しているという課題があります。リプレイスによって、このような課題を解決する予定です。RTM自体が抱えている課題やリプレイス計画については以下のテックブログもあわせてご参照ください。
まとめ
リアルタイムマーケティングシステムの密結合なログ収集から全社ログ収集基盤への移行について紹介しました。ログの見直しによって要件を削減することで、移行の難易度とコストを低減できました。また、完全に移行はできていないものの、部分的なログ収集の疎結合化やセキュリティ向上というメリットを得られました。本記事が皆様の参考になりましたら幸いです。
さいごに
ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください!