TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

987

はじめに こんにちは、MA部MA基盤ブロックの @turbofish_ です。ZOZOTOWNではプッシュ通知やLINE、メール、サイト内お知らせでのキャンペーン配信を行っており、MA部ではそれらの配信を担うマーケティングオートメーション(MA)のシステムを開発しています。本記事ではその中でも、メールの配信を担当する基盤システムをリアーキテクチャし、バッチでの配信とリアルタイムな配信の両立を実現した取り組みをご紹介します。 目次 はじめに 目次 背景・課題 ZOZOTOWNでのキャンペーン配信 メール配信基盤の機能要件 旧メール配信基盤のシステムアーキテクチャと課題 旧メール基盤のアーキテクチャ 旧メール配信基盤の課題 新メール配信基盤の機能要件とアーキテクチャ 新メール配信基盤の機能要件 新メール配信基盤のアーキテクチャ 新メール配信基盤における配信処理の流れ 配信トリガーくん 配信リストアップロードくん 配信してくれるくん 配信完了作業くん リアーキテクチャの結果 まとめ 背景・課題 ZOZOTOWNでのキャンペーン配信 ZOZOTOWNでの配信の特徴はさまざまですが、ここでは配信タイミングを軸として下図のように分類します。 分類 配信ロジック 配信タイミング バッチ配信 事前に決められた条件に当てはまる会員に、決められた時刻に一斉に送信する。 配信開始から終了まで数十分〜数時間の幅があってもよい リアルタイムイベント配信 在庫残り1点や値下げなど、会員に関係するイベントが発生した時に配信する。 データの変更を検知したらできるだけ早く配信する必要がある(配信が遅れるとユーザへのメリットが薄れる)。 上記の分類はメールのみならず全てのチャネルにおいて共通しており、マーケティングメールもその性質によって配信処理に対して許容できる時間に差があります。 キャンペーン配信の分類とMA部で開発しているマーケティングプラットフォームZMPのアーキテクチャについて詳しく知りたい方は、2024年3月に齋藤が執筆した記事をご参照ください。 techblog.zozo.com メール配信基盤の機能要件 メール配信基盤とは、ZOZOTOWNのマーケティングメールの配信を担うシステムです。MA部が開発するマーケティングプラットフォームZMPをはじめ、事前に登録された他システムから配信リクエストを受け取り、リクエストの内容に沿ってメールを配信します。 メールの配信内容は、メールデザインのテンプレートと、キャンペーンやユーザごとに動的な商品などのパラメータにより決まります。 メール配信基盤を使用するシステムは、メール配信基盤に配信リクエストを送る前に、このメールテンプレートと、メールの配信先とメール本文に挿入するパラメータを保存した配信対象者テーブルを準備します。配信対象者テーブルは、CSV形式に変換し、適切な大きさに分割して、配信リストとして外部サービスにアップロードして配信に使用します。 キャンペーン配信時間に期限が存在する場合は、メール配信基盤はリクエストを受信してから期限までに間に合うよう配信処理を行う必要があります。前提として、配信処理にかかる時間は、メール配信数に比例して増加する傾向があります。ZOZOTOWNのメール配信で最も配信量が多い時は、配信リクエストの生成を開始してから全ての送信先に配信されるまでに数時間かかることもあります。そのため、配信数が多いバッチ配信の場合は、それを見越して配信処理にかかる時間を考慮した上で適切な時間に配信されるようスケジューリングしています。 メール配信基盤では、実際にメールを配信する処理は外部サービスをAPI経由で利用しており、その外部配信サービスのAPIの同時実行数には上限があります。よってメール配信基盤も、その上限数以上の並列処理はできません。 旧メール配信基盤のシステムアーキテクチャと課題 この記事では、リアーキテクチャ前の基盤を旧メール配信基盤、リアーキテクチャ後を新メール配信基盤と呼びます。新旧の記載がないメール配信基盤という表記は、リアーキテクチャの前後に関わらない文脈であることを意味します。MA部ではほとんどのシステムをGCP上に構築しており、メール配信基盤も同様にGCPを使用しています。 旧メール基盤のアーキテクチャ 旧メール配信基盤は、DigdagというOSSのワークフローエンジンを使用して実装されており、ワークフロー起動時に配信待ちのリクエストを取得してバッチで処理していました。DigdagはリトライやSLAに加え、プラグインでエラー発生時のSlack通知などの機能をサポートしており、配信処理のロジックの実装のみに集中できる点が大きな魅力でした。旧メール配信基盤のアーキテクチャは下図の通りです。 旧メール配信基盤は、下記の3つのワークフローで構成されていました。 ワークフロー 役割 実行タイミング 起動 - 配信対象のリクエストの取得 - 配信の有効期限チェック - 配信前処理ワークフローの起動 3分おきに定期実行 配信前処理 - 重複配信の制御 - メールアドレスの取得 - 配信リストを外部サービスにアップロード 起動ワークフローから起動される 配信処理 - 外部の配信サービスへのリクエスト - 配信実績の書き込み 5分おきに定期実行 旧メール配信基盤システムの処理やアーキテクチャの詳細について知りたい方は、2024年3月に松岡が執筆した記事をご参照ください。 techblog.zozo.com 旧メール配信基盤の課題 Digdagは1つのジョブが持つ全てのリクエストの配信処理が終わるまで次の処理を開始できません。これにより、配信スループットを決める最大の要因である外部サービスのAPIへのリクエスト量を分散させることができず、リソース効率があまり良くない状態でした。また、大規模なバッチ配信の直後にリアルタイムイベント配信が発生した場合、バッチ配信を捌き切るまで、リアルタイムイベント配信に長い処理待ちが発生してしまう状態でした。 例として、簡単のために外部サービスへの同時リクエスト数上限を2と仮定し、バッチ配信が4リクエスト、リアルタイムイベント配信が1リクエストほぼ同時に作成された場合の処理について考えてみます。旧メール配信基盤では、処理は下図のようになります。バッチ配信のリクエスト中に外部サービスへのリクエストが1並列になっている時間もあるにも関わらず、バッチ配信が全て完了するまでリアルタイムイベント配信が行われません。 以上の理由から、旧メール配信基盤はリアルタイムイベント配信に対応できず、バッチ配信のリクエストのみを処理していました。リアルタイムイベント配信は、MA部の中でも最も古いシステムの1つであるRTMという別のシステムが担っていました。RTMの詳細について知りたい方は、2020年8月に田島が執筆した記事をご参照ください。 techblog.zozo.com メールを配信するシステムが社内に複数存在することの最も重要な弊害として、複数システムから外部の配信サービスへのリクエストの総量が制御できない状態でした。複数のシステムが同じタイミングで配信すると、外部メール配信サービスのサーバーに過剰な負荷がかかり、全ての配信が遅くなってしまうリスクがありました。 新メール配信基盤の機能要件とアーキテクチャ 新メール配信基盤の機能要件 旧メール配信基盤の処理効率の改善に着手できていなかったなか、リアルタイムイベント配信を担っていたRTMをリプレイスするにあたり、RTMからのメール配信においてもメール配信基盤を使用することになりました。よって、新規の要件として下記の2つが求められました。 外部の配信サービスのリソースを最大限活用できるアーキテクチャ リアルタイムイベント配信を優先的に配信できる仕組み 以前より課題だった外部配信サービスの活用効率を改善し、配信全体の処理を最適化する必要がありました。加えて、配信対象が少なく優先度の高いリアルタイムイベント配信が高頻度でリクエストされた場合にも、配信対象が多いバッチ配信の影響を受けることなく配信できる必要があります。こうして、新たな要件に対応すべく、旧メール配信基盤を抜本的にリアーキテクチャすることにしました。 新メール配信基盤のアーキテクチャ 新メール配信基盤では、処理の性質に適した実装にすべくワークフローの形式をやめ、ジョブキューを使ってワーカー的なサーバーが連携して並列処理を行うようリアーキテクチャしました。新メール配信基盤のアーキテクチャは下図のとおりです。 新メール配信基盤を構成する4つのワーカーはCloud RunサービスもしくはCloud Runジョブで作成しており、それぞれの役割は下記の通りです。サーバーを4つに分割したのは、インスタンスごとに同時処理数を制御することに加え、それぞれのワーカーの処理を非同期に行うことで、処理効率を最大化するためです。 ワーカー名 サービス 処理 配信トリガーくん Cloud Runサービス 処理中のタスク数が減少したら、その時最も優先順位が高い配信リクエストを少量ずつ抽出し、ジョブキュー(Cloud Tasks)に詰める。 配信リストアップロードくん Cloud Runサービス 配信リストを外部サービスにアップロードし、完了したら配信リクエストをジョブキュー(Pub/Sub)に詰める。 配信してくれるくん Cloud Runサービス(常時稼働CPU) Pub/Subからメッセージを受け取り、重複配信の制御と配信処理を行う。 配信完了作業くん Cloud Runジョブ 配信が完了したことを確認し、リクエストのステータスを変更したり、配信ログを記録するなどの後処理を行う。 新メール配信基盤における配信処理の流れ 配信トリガーくん 配信トリガーくんは、その時々で配信待ちリクエストの配信優先度を確認し、次に処理するリクエストを決定してCloud Tasksにタスクを送信します。キューの中にある処理待ちのタスクを最小限に保ち、随時その時最も優先度の高いリクエストを少量ずつ追加することで、柔軟な配信順序を実現しています。キューに入ったリクエストは、配信リストアップロードくんが既存のタスクを終えて新規にリクエストを受け付け可能な状態になったら、配信リストアップロード処理を開始します。 配信リストアップロードくん 配信処理の前半は、配信リストのアップロードです。メール配信のフローの中でも、配信前処理の配信リストのアップロードは、そのほかの処理と比較してかなり時間がかかります。さらに、1つのキャンペーンに対する配信先の会員数が多いほど、配信関連データのアップロードにかかる時間は増加する傾向があります。つまり、配信リストアップロードの処理が処理全体のボトルネックになります。この配信リストアップロード処理の外部APIを同時リクエスト数上限まで活用できるよう、配信リストアップロードくんはインスタンスが処理する同時リクエスト数に応じてオートスケールし、並列で処理を行います。オートスケール時の最大インスタンス数とCloud Runの最大同時実行リクエスト数の設定を掛けた数が、外部サービスAPIの同時リクエスト数上限を超えないよう設定しています。配信リストのアップロードの処理が完了したらPub/Subにメッセージをパブリッシュし、新しいリクエストを受け付けます。 配信してくれるくん 配信してくれるくんは、受信したリクエストが重複配信ではないことを確認してから、メール配信APIを実行します。外部の配信サービスから取得した配信履歴を確認することで、同じ配信先に対して複数回同じメールを配信してしまうことを防いでいます。 配信完了作業くん 配信完了作業くんは、配信してくれるくんがメール配信を行った後の処理を担当します。まず、外部サービスのAPIを叩いて、メール配信ステータスが配信済みに更新されたことを確認します。配信完了を確認したら、データベースに配信実績を記録し、配信リクエストのステータスを更新するなどの後処理を行います。配信処理と配信後の処理がキューを挟んで分かれていることで、配信ステータスの確認を行なっている間にも、配信してくれるくんは後続のリクエストを処理して配信処理を行うことができます。 リアーキテクチャの結果 新メール配信基盤では、配信量が多いバッチ配信と断続的に発生する少量のリアルタイムイベント配信を同時に実行しても、全ての配信においてそれぞれの要件を満たす時間内で配信ができるようになりました。また、配信全体の処理効率が向上していることも確認できました。 旧メール基盤のアーキテクチャの説明と同様の状況を例に説明します。同時実行数の上限が2のとき、バッチ配信4、リアルタイムイベント配信1リクエストが追加された状況で、新旧基盤での処理は下図のように異なります。 処理効率の向上については、想定していた以上の改善が見られました。負荷テストの結果、バッチ配信で新旧基盤を用いて同じ配信数を捌き切るまでの時間を計測したところ、配信処理の開始から配信完了までの時間がおよそ40%短縮されました。 また、メール配信基盤がメール配信処理を一括で担うことにより、社内の他システムがメール配信のロジックを再実装する必要がなくなり、外部の配信サービスへの負荷もメール配信基盤で制御できるようになります。 まとめ 本記事では、メール配信システムのリアーキテクチャの事例を紹介しました。メール配信基盤は、ワークフローエンジンを用いたバッチ処理からワーカーをジョブキューで連携させ並列処理を行う非同期的なアーキテクチャに変更しました。これにより、処理効率を大幅に向上させ、配信の性質に応じて柔軟に処理を進めるシステムへと進化しました。アプリケーションの再実装は工数もかかりましたが、システムの最適化によってマーケティングメール配信が管理しやすくなり、保守面でも大きな恩恵がありました。大量のリクエストを処理する必要があるシステムの開発、スケーラブルで処理効率が高いアプリケーション設計に興味がある方の参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。2025年4月に新卒で株式会社ZOZO(以下、ZOZO)に入社予定の清板海斗(せいたかいと)です。2024年8月から入社までの約半年間、「WEAR by ZOZO」(以下、WEAR)のiOSチームで内定者アルバイトに参加しました。この記事では、内定者アルバイトの目的やチームでの取り組み、全体の振り返りについてご紹介します。 目次 はじめに 目次 内定者アルバイトの概要 ZOZOの内定者アルバイトについて 内定者アルバイトでの働き方 WEARとは WEAR iOSチームについて 主な取り組み MVPへのリアーキテクチャ タスク概要 Presenter移行の背景 WEAR iOSにおけるPresenter 実施したこと 結果 学び 1. コミュニケーションと言語化の重要性 2. UIKitの理解 3. 実務レベルで求められる視点 最後に 内定者アルバイトの概要 ZOZOの内定者アルバイトについて 内定者アルバイトとは、内定承諾から入社までの期間にアルバイトとして就業できるものです。また、選考時に志望していた職種以外の求人に応募できます。職種以外にもサービスごとで募集もしているので入社後に希望している配属先のサービス以外の選択肢もあり、様々なサービスの雰囲気を知ることができる良い機会になると思います。 内定者アルバイトでの働き方 私は2024年2月に内定をいただき、同年の8月から翌年の3月の大学卒業までの8か月間、WEARのiOSアプリを開発するチームで内定者アルバイトとして働きました。 WEARとは ZOZOはWEARという日本最大級のファッションコーディネートサイトを運営しています。2024年の5月に「WEAR by ZOZO」として「似合う」が探せるアプリを目指してリニューアルしました。 WEAR iOSチームについて WEAR iOSチームは10名程度で構成されており、チームのコミュニケーションはSlack、Google Meet、Discord等で行われています。案件ごとでチームがさらに細分化されており、改善に向けた取り組みはその中で行われることが多いです。 iOSチーム全体では次のような活動をしています。 朝会 連絡事項の共有 雑談(ご飯、スニーカー、格闘技などの話題が多く、個性的な雰囲気) チーム定例 技術的な知見、調査内容の報告など 登壇の発表練習 Findyの数値を見ながら開発パフォーマンスの確認 Warning数など技術負債のモニタリング タスク、PRの細分化に関する相談、共有 1on1 配属時にチームメンバー全員と1on1 お互いを知るための雑談 年齢層が高めのチームで、相談には若い頃のお話も交えて親身に乗っていただけますし、ミスがあっても優しく指摘していただけます。とにかくアットホームなチームです。 主な取り組み 最初は、UIやバグの修正などの簡単なタスクから始め、徐々にテストの実装やリアーキテクチャといった難しいタスクに取り組みました。 デザイン修正 ボタンの位置の修正などの対応 バグ修正 年月日表記の修正対応 warning解消 Swift 6対応に伴うXcodeのwarning解消 リプレイス 独自実装を UICollectionViewCell の automaticallyUpdatesContentConfiguration を使用するようにリプレイス 独自実装を UICollectionViewCell の標準APIを使用するようにリプレイス リアーキテクチャ Presenter移行 その中でも特に印象的だったタスクについてご紹介します。 MVPへのリアーキテクチャ タスク概要 「ノウハウ動画投稿の着用コーディネートアイテム画面」にMVPアーキテクチャを適用しました。この画面は、ホームや探すタブからアクセスできる「ノウハウ動画」コンテンツの中心的な機能の1つです。 WEARのiOSアプリでは、MVPアーキテクチャを採用していますが、今回の対象のように適用されていない画面もあります。中心的な機能であるため、今後も改修の影響が考えられることもあり、今回のタイミングでリアーキテクチャに取り組みました。 Presenter移行の背景 アーキテクチャが適用されていない従来の画面では、責務の分離ができていないという課題がありました。これにより、以下の問題が発生していました。 画面のコードが肥大化し、可読性が低下 テストがしづらく、品質を担保しにくい 修正時に影響範囲が広がりやすい MVPアーキテクチャが導入されていないため、ViewControllerに全ての処理を実装することで、様々な処理を同時に複数抱えた、いわゆる「FatViewController」になっていました。 そこで、MVPアーキテクチャを導入し、責務を明確に分離することで、これらの課題を解決することを目指しました。 WEAR iOSにおけるPresenter Presenterは、Viewの状態管理とアクション処理を行う責務を持つコンポーネントとして定義しています。各画面の一貫性を保つため、Presenterプロトコルに準拠しています。 Presenterの構成は次の通りです。 ViewState UIを構築するための状態(値のみを持ち、副作用を排除) InputAction Viewからのアクション(ユーザー操作やライフサイクルイベント) OutputAction Viewへの命令(画面更新や遷移指示) Presenter InputActionやOutputActionに対応した処理 ViewStateを変更 実際のコードは次の通りです。 protocol Presenter < ViewState , InputAction , OutputAction >: Sendable { associatedtype ViewState associatedtype InputAction associatedtype OutputAction var state : ViewState { get } var output : (( OutputAction ) -> Void ) ? { get } func apply (outputAction output : @escaping (( OutputAction ) -> Void ) ) func send (_ action : InputAction ) async } ViewStateを追加して状態管理を分離することで、Presenter本体にはInputActionおよびOutputActionに対応した処理だけを実装でき、役割がさらに明確になります。 実施したこと Presenter移行方針と単一責任の原則に基づき、以下の対応をしました。 Presenterの追加(ロジックをViewから分離) ViewState、InputAction、OutputActionの追加(責務の明確化) テストの追加(動作保証とリファクタリングのしやすさ向上) ログ送信をPresenter側に移行 結果 Presenterの導入前後でどのようにコンポーネントが変わったのかを以下に示します。 まず、わかりやすくするためにコンポーネントをUI、仲介役、データ処理の3つの役割に分けます。Stateは状態を保持する補助的なものとします。 次に、実際のコンポーネントをそれぞれの役割で分類し、Presenter導入前後の関係図を示します。 【Before】 データ処理の部分にあるFetcherなどは実際に使用している処理を担当するクラス名です。示したもの以外にも存在し、責務が最小化された複数のクラスに分割しています。 小さい規模のプロジェクトであればこのような構成でも問題ないですが、Viewのイベントの数が多いほどViewControllerの仕事が増えてしまい、「Fat」になってしまいがちです。 【After】 ViewControllerを仲介役から切り離し、Presenterにデータの加工、ログの送信などを任せることで一気に見通しが良くなりました。 学び 内定者アルバイトを通して学んだことは、大きく3点あります。 1. コミュニケーションと言語化の重要性 タスクに慣れていないうちは、問題点や解決方針を明確に言語化できず、メンターやレビュー担当者に負担をかけてしまっていました。 タスクに取り組むスピードも大切ですが、それ以上に チームの一員として、普段から言語化を意識し、相手の負荷を減らす習慣をつけることが重要 だと学びました。 2. UIKitの理解 自分は学生時代の多くの時間をSwiftUIに費やしていたのでUIKitに慣れていませんでした(同じような学生の方はこれから増えていくのではないでしょうか?)。以下のような実装に触れましたが、UIKitをほとんど一から触ることとなり、手探り状態でとても苦労しました。 UICompositionalLayout UICollectionView UIContentConfiguration 宣言型のSwiftUIしか触れていないと手続き型の実装方法はとても複雑に感じ、理解にも時間がかかります。そのため何度も社員の方々にペアプログラミング、コードレビューをお願いしてしまいましたが、本当に親切に対応してくださる方ばかりで手探り状態の自分でもなんとか乗り越えることができ、本当に感謝しています。ZOZOの人々の良さを改めて実感しました。 3. 実務レベルで求められる視点 仕事としてのプログラミングでは、以下の点が特に重要だと感じました。 チームの生産性を意識した開発 大規模サービスならではのアーキテクチャ設計 責務の最小化と適切な分離 コードを書くことだけでなく、 長期的なメンテナンスやチーム開発を意識した設計が求められる ことを実感しました。 最後に 長期インターンを継続している方や内定者期間に他社でエンジニアのアルバイトをしている方もいますが、自分はそうでなく、技術的なことや仕事としての立ち回りなどの不安がありました。そんな中でこのような就業機会をいただけたことで、不安も払拭され、入社後のイメージもはっきりして実現したいことがいっぱいです。もし内定をもらって自分のような境遇の方がいましたらぜひ内定者アルバイトに挑戦してほしいです! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
はじめに こんにちは、WEARバックエンド部バックエンドブロックの伊藤です。普段は弊社サービスである WEAR のバックエンド開発・保守を担当しています。 WEARでは、天気予報データを活用してその日の天気に合わせたコーディネートを提案する「コーデ予報」機能を提供しています。リリース当初はコーデ予報の地域を一覧から選んで設定する必要がありましたが、2025年1月にユーザーの位置情報をもとにコーデ予報の地点を自動設定する機能をリリースしました。 本記事では、ユーザーの現在地から最寄りのコーデ予報地点を取得するために使用したアルゴリズムの詳細をご紹介します。 目次 はじめに 目次 コーデ予報とは? 背景・課題 ユーザーの位置情報から最寄りの地点をどのように特定するか? 1.ユーザーの位置情報を基に検索範囲を絞る 2.範囲内の各地点との距離を計算 3.最も近い観測地点を特定 4. 全体のアルゴリズム 動作確認 リリース後 まとめ コーデ予報とは? コーデ予報とは、気温や降水確率などの天気情報をもとに、その日のコーディネートを提案する機能です。WEARでは、ウェザーニューズが提供するWxTechの高解像度かつ高精度な「1kmメッシュ体感予報」を活用し、地域ごとの体感指数を取得しています。 体感指数とは、ウェザーニューズに寄せられたユーザーの体感報告と、天気・気温・湿度・風速などの気象データを分析し、「暑い」「寒い」「ちょうどよい」などの人の体感を10ランクに分類した指数です。この体感指数をもとに、「半袖」「長袖」などの袖丈やアイテムカテゴリーのおすすめを判別し、気温や体感に応じたアイテムやコーディネートを提案しています。 背景・課題 コーデ予報では、どの地域の天気情報を取得し、コーディネートを提案するかを設定します。リリース当初は、WEARアプリ内の地域設定で、地域一覧から手動で選択する必要がありました。設定できる地域は、各都道府県の代表的な都市を中心に103地点あります。 しかし、103地点の中から最寄りの地域を探すのは手間がかかり、直感的に判断するのも難しいという課題がありました。特に、自分の住んでいる地域が候補に含まれていない場合、どの地点を選べばよいのか迷ってしまうこともありました。その結果、地域設定するユーザーが限られ、十分に活用されないケースもありました。 そこで、位置情報を利用して最寄りの地域を自動設定する機能を導入し、よりスムーズにコーデ予報を活用できるようにしました。 ユーザーの位置情報から最寄りの地点をどのように特定するか? 103地点の位置情報はウェザーニューズから取得し、ActiveYamlで管理しています。 class WeatherMunicipality < ActiveYaml :: Base set_filename ' weather_municipalities ' field :id , type : :integer field :name , type : :string field :latitude , type : :float field :longitude , type : :float end # weather_municipalities.yml - id : 1 name : 札幌市中央区 latitude : 43.062638 longitude : 141.353921 - id : 2 name : 函館市 latitude : 41.768702 longitude : 140.728938 ... また、ユーザーの位置情報はデータベースに保持せず、リクエストごとに取得します。 WEARではSQL Serverを使用しているため、各地点の位置情報をGEOGRAPHY型でデータベースに保持することで、 location.STDistance を用いて地点間の距離を計算できます。 しかし、今回はActiveYamlで各地点の位置情報を管理しているため、Rubyのみを使用してユーザーの位置情報と各地点との距離を計算するアルゴリズムを実装しました。 1.ユーザーの位置情報を基に検索範囲を絞る ユーザーの現在地から最寄りの地点を探す際、すべての地点との距離を計算するのは非効率です。そこで、まずはユーザーの端末の位置情報をもとに所在する都道府県を特定し、その都道府県内の地点との距離のみを計算する方法を検討しました。 しかし、この方法では、県境付近のユーザーが隣接する県の地点のほうが近い場合でも、都道府県内の地点が優先されて正確に最寄りの地点を特定できない可能性があります。 次に、緯度経度をもとにエリアを均等に分割する「格子メッシュ」を導入し、各地点をあらかじめ格子メッシュに割り当てておく方法を検討しました。ユーザーの位置情報が属する格子メッシュを判定し、その格子メッシュ内の地点との距離のみを計算する仕組みです。 しかし、この方法でも、格子メッシュの境界付近にいる場合は最寄りの地点を正しく選べない可能性があるため、最適とは言えません。 そこで、バウンディングボックスを導入することにしました。バウンディングボックスとは、対象を囲む最小の矩形(四角形)を指します。通常、最小緯度・最小経度・最大緯度・最大経度の4点で定義されます。 今回は、ユーザーの現在地を中心に、指定した半径(km)の範囲をカバーする緯度経度の範囲をバウンディングボックスとして設定します。 バウンディングボックスの計算方法は以下の通りです。 (1)緯度の変化量 (2)経度の変化量 (3)バウンディングボックス ここで、 :ユーザーの現在地からの半径(km) :地球の平均半径(6371.0km) Rubyで実装すると、次のようになります。 # 地球の平均半径(km) EARTH_RADIUS = 6371.0 # @param latitude [Float] 緯度 # @param longitude [Float] 経度 # @param distance_km [Float] 半径(km) # @return [Hash] 矩形範囲 def bounding_box ( latitude :, longitude :, distance_km :) delta_lat = distance_km / EARTH_RADIUS * ( 180 / Math :: PI ) delta_lon = distance_km / ( EARTH_RADIUS * Math .cos(latitude * Math :: PI / 180 )) * ( 180 / Math :: PI ) { min_lat : latitude - delta_lat, max_lat : latitude + delta_lat, min_lon : longitude - delta_lon, max_lon : longitude + delta_lon } end バウンディングボックス内の地点は、緯度経度の範囲内にある地点を取得することで絞り込めます。以下のように実装できます。 # @param latitude [Float] ユーザーの緯度 # @param longitude [Float] ユーザーの経度 # @param search_radius_km [Float] 検索半径(km) # バウンディングボックスを定義 box = bounding_box( latitude : latitude, longitude : longitude, distance_km : search_radius_km) # バウンディングボックス内の地点を取得 weather_municipalities = all.select do |weather_municipality| weather_municipality.latitude.between?(box[ :min_lat ], box[ :max_lat ]) && weather_municipality.longitude.between?(box[ :min_lon ], box[ :max_lon ]) end この方法により、ユーザーの現在地を中心に指定した半径(km)以内の地点を効率的に絞り込むことができ、県境などに関係なく正確な最寄り地点を特定できます。 また、バウンディングボックス内に地点がない場合は、半径を拡大して再検索することで最寄りの地点を取得できます。地点の数や分布に応じて適切な初期半径を設定し、徐々に拡大することで、効率的に最寄りの地点を見つけることができます。 WEARでは、初期半径を50kmに設定し、50km刻みで半径を広げながら検索し、最大2000kmまで拡張できるようにしました。 2.範囲内の各地点との距離を計算 次に、バウンディングボックス内の各地点とユーザーの位置情報との距離を計算します。 地球上の2点間の距離は、球面上の2点間の最短距離を計算するための数学的公式である「ハーバサインの公式」を使用して求めます。ハーバサインの公式は、以下のように表されます。 (1) (角度差の二乗和) (2) (大円距離の角度) (3) (実際の地球上の距離) ここで、 :地点1と地点2の緯度の差(ラジアン単位) :地点1と地点2の経度の差(ラジアン単位) :地点1の緯度(ラジアン単位) :地点2の緯度(ラジアン単位) :地球の平均半径(6371.0km) 上記をRubyで実装すると、次のようになります。 # 地球の平均半径(km) EARTH_RADIUS = 6371.0 # @param lat1 [Float] 地点1の緯度(ラジアン単位) # @param lon1 [Float] 地点1の経度(ラジアン単位) # @param lat2 [Float] 地点2の緯度(ラジアン単位) # @param lon2 [Float] 地点2の経度(ラジアン単位) # @return [Float] 2点間の距離(km) def haversine_distance ( lat1 :, lon1 :, lat2 :, lon2 :) delta_lat = lat2 - lat1 delta_lon = lon2 - lon1 a = ( Math .sin(delta_lat / 2 )** 2 ) + ( Math .cos(lat1) * Math .cos(lat2) * ( Math .sin(delta_lon / 2 )** 2 )) c = 2 * Math .atan2( Math .sqrt(a), Math .sqrt( 1 - a)) EARTH_RADIUS * c end 3.最も近い観測地点を特定 バウンディングボックス内の各地点とユーザーの位置情報との距離をすべて計算し、最小となる観測地点を特定します。 weather_municipalities.min_by do |weather_municipality| haversine_distance( lat1 : latitude * Math :: PI / 180 , lon1 : longitude * Math :: PI / 180 , lat2 : weather_municipality.latitude * Math :: PI / 180 , lon2 : weather_municipality.longitude * Math :: PI / 180 ) end 4. 全体のアルゴリズム 上記の手順をまとめると、次のようになります。 class WeatherMunicipality < ActiveYaml :: Base set_filename ' weather_municipalities ' field :id , type : :integer field :name , type : :string field :latitude , type : :float field :longitude , type : :float EARTH_RADIUS = 6371.0 class << self # @note 指定した緯度経度に最も近い地点を取得する(検索半径を徐々に広げて地点を探し、max_radius_kmに達した場合はnilを返す) # @param latitude [Float] ユーザーの緯度 # @param longitude [Float] ユーザーの経度 # @param initial_radius_km [Integer] 検索半径の初期値(km) # @param max_radius_km [Integer] 検索半径の最大値(km) # @param radius_step_km [Integer] 検索半径のステップ(km) # @return [WeatherMunicipality, nil] 最も近い地点 def nearest ( latitude :, longitude :, initial_radius_km : 50 , max_radius_km : 2000 , radius_step_km : 50 ) search_radius_km = initial_radius_km loop do # バウンディングボックスを定義 box = bounding_box( latitude : latitude, longitude : longitude, distance_km : search_radius_km) # バウンディングボックス内の地点を取得 weather_municipalities = all.select do |weather_municipality| weather_municipality.latitude.between?(box[ :min_lat ], box[ :max_lat ]) && weather_municipality.longitude.between?(box[ :min_lon ], box[ :max_lon ]) end if weather_municipalities.any? # 地点が1つだけの場合はその地点を返す return weather_municipalities.first if weather_municipalities.size == 1 # 2つ以上の地点がある場合はハーバサインの公式で距離を計算し、最も近い地点を返す return weather_municipalities.min_by do |weather_municipality| haversine_distance( lat1 : latitude * Math :: PI / 180 , lon1 : longitude * Math :: PI / 180 , lat2 : weather_municipality.latitude * Math :: PI / 180 , lon2 : weather_municipality.longitude * Math :: PI / 180 ) end end # バウンディングボックス内に地点がない場合は、検索半径を広げて再度検索 search_radius_km += radius_step_km # 検索半径が最大値に達した場合は終了 break if search_radius_km > max_radius_km end nil end private # @note 緯度経度から指定した距離の矩形範囲を求める # @param latitude [Float] 緯度 # @param longitude [Float] 経度 # @param distance_km [Float] 半径(km) # @return [Hash] 矩形範囲 def bounding_box ( latitude :, longitude :, distance_km :) delta_lat = distance_km / EARTH_RADIUS * ( 180 / Math :: PI ) delta_lon = distance_km / ( EARTH_RADIUS * Math .cos(latitude * Math :: PI / 180 )) * ( 180 / Math :: PI ) { min_lat : latitude - delta_lat, max_lat : latitude + delta_lat, min_lon : longitude - delta_lon, max_lon : longitude + delta_lon } end # @note 2点間の距離をハーバサインの公式で求める # @param lat1 [Float] 地点1の緯度(ラジアン単位) # @param lon1 [Float] 地点1の経度(ラジアン単位) # @param lat2 [Float] 地点2の緯度(ラジアン単位) # @param lon2 [Float] 地点2の経度(ラジアン単位) # @return [Float] 2点間の距離(km) def haversine_distance ( lat1 :, lon1 :, lat2 :, lon2 :) delta_lat = lat2 - lat1 delta_lon = lon2 - lon1 a = ( Math .sin(delta_lat / 2 )** 2 ) + ( Math .cos(lat1) * Math .cos(lat2) * ( Math .sin(delta_lon / 2 )** 2 )) c = 2 * Math .atan2( Math .sqrt(a), Math .sqrt( 1 - a)) EARTH_RADIUS * c end end end 動作確認 上記のアルゴリズムを実装し、バウンディングボックス内に6地点が含まれるケースで、ユーザーの現在地が千葉県・松戸市(緯度:35.750169、経度:139.91816)の場合を検証しました。 この場合、最寄りの地点として東京都・千代田区が返却されることを期待します。 以下が実行結果です。 ❯ rails c [1] pry(main)> require 'benchmark' => false [2] pry(main)> [3] pry(main)> # 千葉県松戸市の緯度経度 [4] pry(main)> lat,lon = 35.750169,139.91816 => [35.750169, 139.91816] [5] pry(main)> nearest = nil => nil [6] pry(main)> [7] pry(main)> time = Benchmark.realtime do [7] pry(main)* nearest = WeatherMunicipality.nearest(lat, lon) [7] pry(main)* end => 0.00025700032711029053 [8] pry(main)> [9] pry(main)> puts "最寄りの地点: #{nearest.region.region_name} #{nearest.name}\t実行時間: #{time} seconds" 最寄りの地点: 東京都 千代田区 実行時間: 0.00025700032711029053 seconds => nil 想定通り、最寄りの地点として東京都・千代田区が返却されました。実行時間は0.000257秒と高速に処理されています。 リリース後 上記のアルゴリズムを導入し、2025年1月にユーザーの位置情報から最寄りのコーデ予報地点を選択できる機能を無事にリリースしました。 地域設定のUX向上により、地域登録数がリリース前の6〜7倍に増加し、大きな成果を得られました。 また、APIのレイテンシは平均20ms程度と高速に処理されており、リソース使用状況も安定しているため、ユーザーがストレスなく利用できる状態を維持しています。 まとめ 本記事では、WEARのコーデ予報機能において、ユーザーの位置情報から最寄りの地点を取得するアルゴリズムについて紹介しました。 まず、地点間の距離を効率よく計算するために、バウンディングボックスを使って候補を絞り、その後、ハーバサインの公式で正確な距離を算出しました。この方法により、高速かつ正確に最寄りの地点を特定できました。 WEARでの実装例を紹介しましたが、データの保持方法や地点数、サービスの特性に応じて最適なアルゴリズムは異なります。WEARの事例が参考になれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
ZOZO開発組織の2025年2月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2025年2月は、前月のMonthly Tech Reportを含む計8本の記事を公開しました。昨年に続き、今年も NRF Retail's Big Show のレポート記事を掲載しています。ぜひご覧ください。 techblog.zozo.com ZOZO DEVELOPERS BLOG 計測プラットフォーム開発本部 計測システム部の児島と髙橋が、計測システム部の「現在の取り組み」を紹介する記事を公開しました。海外の開発拠点であるZOZO NEW ZEALANDとの関わり方についても触れています。ぜひご一読ください。 technote.zozo.com 登壇 Developers Summit 2025 2月13日に開催された『 Developers Summit 2025 』で、技術戦略部の諸星( @ikkou )がパネルディスカッション「 「Apple Vision Pro」の登場で何が変わるのか? ITエンジニアとXRの可能性を語ろう 」のモデレーターを務めました。 2/13~14にホテル雅叙園東京で開催されるDevelopers Summit 2025に技術戦略部の諸星 @ikkou がパネルディスカッション『「Apple Vision Pro」の登場で何が変わるのか? ITエンジニアとXRの可能性を語ろう』のモデレーターとして登壇します🎙️ https://t.co/rzcXttn1a7 #devsumi #zozo_engineer — ZOZO Developers (@zozotech) 2025年2月12日 ZOZO Tech Meetup ~データサイエンス~ 2月14日に開催された『 ZOZO Tech Meetup ~データサイエンス~ 』に、3名のZOZOエンジニアが登壇しました。 【ZOZOエンジニア登壇情報】 2/14(金)開催の『ZOZO Tech Meetup ~データサイエンス~』に、ビジネスアナリティクス部に所属するデータサイエンティスト 茅原、橘、佐々木の3名が登壇します🎙️ https://t.co/6uT5E0xUP8 #zozo_ds — ZOZO Developers (@zozotech) 2025年2月13日 茅原: 因果推論が浸透した組織の現状と未来 橘: 難題に挑むデータアナリティクス:意思決定を支える分析の舞台裏 佐々木: 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話 イベントレポートには、登壇した3名全員の資料も掲載しています。ぜひご覧ください。 techblog.zozo.com Tokyo dbt Meetup #12 2月21日に開催された『 Tokyo dbt Meetup #12 』で、データシステム部の栁澤( @i_125 )が「 システム・ML活用を広げるdbtのデータモデリング 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 2/21(金)開催の『Tokyo dbt Meetup #12』に、データシステム部のAnalytics Engineerである栁澤 @i_125 が『システム・ML活用を広げるdbtのデータモデリング』というタイトルで登壇します🎙️ https://t.co/Pt9SxcAL06 #dbt_tokyo — ZOZO Developers (@zozotech) 2025年2月20日 speakerdeck.com Elasticsearch Community in Tokyo 2025 2月26日に開催された『 Elasticsearch Community in Tokyo 2025 』で、SRE部の花房が「 Elastic Cloudの特権アカウント共用から脱却! 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 本日2/26(木)開催の『Elasticsearch Community in Tokyo』に、SRE部の花房が「Elastic Cloudの特権アカウント共用から脱却!」というタイトルで登壇します🎙️ Elasticsearchに関心のある方はぜひご参加ください! https://t.co/S8eE0XQp98 #elasticsearchjp #elasticsearch — ZOZO Developers (@zozotech) 2025年2月26日 speakerdeck.com 【SRE特集】3社が語るSREの役割 組織の変遷と直面した課題とは? 2月27日に開催された『 【SRE特集】3社が語るSREの役割 組織の変遷と直面した課題とは? 』で、SRE部基幹プラットフォームSREブロック ブロック長の柴田が「 ZOZOTOWNにおけるSREの変遷と現在 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 本日2/27開催の『【SRE特集】3社が語るSREの役割 組織の変遷と直面した課題とは?』に、SRE部の柴田が「ZOZOTOWNにおけるSREの変遷と今後」というタイトルで登壇します🎙️ ZOZOTOWNのSREに興味をお持ちの方はぜひご参加ください! https://t.co/h4w0uJVNot #JobLT_findy — ZOZO Developers (@zozotech) 2025年2月27日 掲載 日本ネット経済新聞 EC&流通のデジタル化をリードする専門紙『日本ネット経済新聞』のAI特集記事で、AI・アナリティクス本部 本部長の牧野が取材を受け、ZOZOTOWNやWEAR by ZOZOにおけるAIの活用事例が紹介されました。 netkeizai.com 「Girls Meet STEM〜ITのお仕事を体験しよう〜」開催報告 2024年12月に実施した「Girls Meet STEM〜ITのお仕事を体験しよう〜」の開催報告が公開され、ZOZOの取り組みも紹介されています。 prtimes.jp Girls Meet STEMの取り組みについてはイベントレポートも公開していますので、あわせてご覧ください。 techblog.zozo.com 以上、2025年2月のZOZOの活動報告でした! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、MA部MA開発ブロックの平井です。普段はマーケティングオートメーションシステムの運用、開発を担当しています。現在、開発ブロックではリアルタイムマーケティングシステムのリプレイスプロジェクトに取り組んでいます。リプレイスプロジェクトを進める上で、性能目標を満たすためにアプリケーションのパフォーマンスチューニングが必要でした。今回、Cloud Traceを利用してアプリケーションパフォーマンスを可視化し、パフォーマンスチューニングを行ったためその知見を共有したいと思います。 この記事の内容を読むと、以下の内容について知ることができます。 Cloud Run上の処理をCloud Traceを用いて可視化する方法 Cloud Traceでトレース情報を確認する方法 Cloud Traceをパフォーマンスチューニングに活用した一例 目次 はじめに 目次 背景 課題 Cloud Traceについて Cloud Traceで処理を可視化するために アプリケーションの計装 Google Cloudサービスの各トレース情報紐付け Cloud Run AlloyDB Pub/Sub Cloud Logging Cloud Trace上でのトレース情報の見え方 AlloyDB Pub/Sub Cloud Logging パフォーマンスチューニング データベースへの最大接続数の調整 Pub/Subメッセージパブリッシュ数の削減 クエリ実行回数の削減 まとめ 背景 本記事の本題へ入る前にリプレイス先のシステムについて説明します。私たちMA部ではインフラにGoogle Cloudを利用することが多く、今回開発しているシステムもGoogle Cloudを利用しています。Cloud Run上に構築された複数のアプリケーションがPub/Subで連携し1つのシステムを構成しています。また、メインのデータベースとしてAlloyDBを利用しています。 今回は、システムを構成している個々のアプリケーションのパフォーマンス改善ポイントを特定するためにCloud Traceを利用しました。 課題 パフォーマンスチューニングを行う上で、Cloud Traceを利用するまでは以下のような課題がありました。 アプリケーション内で実際にどのような処理が実行されているかわかりづらい。 アプリケーション内の各処理の実行時間がわからない。 また、「アプリケーション内の各処理の実行時間がわからない」という課題について、具体的に実現したいことは以下です。 アプリケーション上で実行されるクエリ実行時間を知りたい。 Pub/Subメッセージのパブリッシュにかかる時間を知りたい。 Cloud Loggingに連携したログをもとに特定処理の実行時間を知りたい。 Cloud Traceを利用することで以上のような課題を解決できました。 Cloud Traceについて 今回利用した Cloud Trace はGoogle Cloudが提供する分散トレースシステムです。分散トレースシステムを活用することで、マイクロサービスシステムの処理の流れを把握できます。しかし、今回の目的はマイクロサービス全体の処理を把握することではなく、アプリケーションのパフォーマンスチューニングだったため、APM(アプリケーションパフォーマンス監視機能)として利用しました。 Cloud Traceに関する詳細な情報については 公式ドキュメント を確認してください。 APMを提供するサービスは多数ありますが、システムがGoogle Cloud上に構築されていて、利用しているサービスとの親和性が高く簡単に導入できることから、Cloud Traceを選択しました。 Cloud Traceで処理を可視化するために Cloud Traceを利用して適切に処理を可視化するためには、以下を実現する必要がありました。 アプリケーションの計装 Google Cloudサービスの各トレース情報紐付け これから、これらのポイントについて実際のアプリケーションコードを使って説明します。今回実装したシステムではGoを使用しているため、説明に使用するのはGoのソースコードです。 アプリケーションの計装 「計装」とは分散トレーシングの文脈でよく使用される用語です。「アプリケーションを計装」するとは、アプリケーションにトレース機能を組み込み、トレース情報を収集できるようにすることを指します。今回実装したシステムではOpenTelemetryフレームワークを利用して計装を実現しています。GoでOpenTelemetryを構成するために必要な設定については 公式ドキュメント を参考にしてください。 Google Cloudサービスの各トレース情報紐付け Cloud Run Cloud Runではリクエスト受信時に自動でトレース情報を生成します。ただ、アプリケーションにOpenTelemetryを構成したとしても自動生成されたトレース情報がアプリケーションのトレース情報と紐づかないため処理を可視化するには不十分です。 今回実装したシステムではルーティングライブラリに chi を利用しています。以下のようにotelhttpのハンドラをミドルウェアとして登録することで、Cloud Runが自動生成したトレース情報とアプリケーションのトレース情報が紐づきます。 func tracingHandler(next http.Handler) http.Handler { return otelhttp.NewHandler(next, "http-request" ) } func NewRouter(conf *config.Config) (*chi.Mux, error ) { //その他のコードが続く //otelhttpハンドラの登録 router.Use(tracingHandler) //その他のコードが続く return router, nil } AlloyDB AlloyDBで実行されたクエリはトレース情報として自動的にCloud Traceへ送信されます。ただ、何も設定しない状態だとCloud Runが自動生成したトレース情報と紐づいていないため、完全に別の処理として認識されてしまいます。今回実装したシステムではGORMをO/Rマッパーとして利用しているため、GORMが用意している go-gorm/opentelemetry を利用しました。 以下が実際のアプリケーションコードを簡素化したものです。 func NewPostgresHandler(config *config.Config) *PostgresHandler, err { dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Tokyo" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: customLogger, }) if err != nil { return nil , err } //その他のコードが続く err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics())) if err != nil { return nil , err } //その他のコードが続く } func (h *PostgresHandler) GetDB(ctx context.Context) *gorm.DB { tx, ok := ctx.Value( "transaction" ).(*gorm.DB) if ok { return tx } return h.db.WithContext(ctx) } NewPostgresHandler 関数ではDB接続設定をしています。 db.Use(tracing.NewPlugin(tracing.WithoutMetrics())) の部分でopentelemetryライブラリを利用することを宣言しています。 GetDB はリポジトリ層から呼び出される *gorm.DB を返すメソッドです。重要なのは最後の return h.db.WithContext(ctx) でContextを渡している部分です。Contextを利用してトレース情報を伝播するため *gorm.DB に渡す必要があります。 Pub/Sub Pub/Subに関してもトレース情報は自動生成されます。そのため、アプリケーションのトレース情報と自動生成されるトレース情報を紐づける必要があります。 func PublishMessage(ctx context.Context, projectID string , message *pubsub.Message, isLoggingBody bool ) error { client, err := pubsub.NewClientWithConfig(ctx, projectID, &pubsub.ClientConfig{ EnableOpenTelemetryTracing: true , }) if err != nil { return err } defer client.Close() t := client.Topic(topicID) defer t.Stop() result := t.Publish(ctx, message) //その他のコードが続く } PublishMessage はPub/Subメッセージをパブリッシュする関数です。 NewClientWithConfig 関数でクライアントを初期化する際に EnableOpenTelemetryTracing を true に指定する必要があります。この設定をすることでメッセージAttributesの googclient_traceparent フィールドに親トレースIDが入り、このフィールドの値をもとにトレース情報を伝播できます。 Cloud Logging Cloud Loggingに連携したログデータをトレース情報として紐づけるためには、構造化ログの 特定フィールド に適切な値を設定する必要があります。 type TraceHandler struct { slog.Handler project string } func (h *TraceHandler) Handle(ctx context.Context, record slog.Record) error { path := "projects/" + h.project + "/traces/" if s := trace.SpanContextFromContext(ctx); s.IsValid() { record.AddAttrs( slog.String( "logging.googleapis.com/trace" , path+s.TraceID().String()), slog.String( "logging.googleapis.com/spanId" , s.SpanID().String()), slog.Bool( "logging.googleapis.com/trace_sampled" , s.TraceFlags().IsSampled()), ) } return h.Handler.Handle(ctx, record) } func NewTraceHandler(baseHandler slog.Handler, project string ) *TraceHandler { return &TraceHandler{Handler: baseHandler, project: project} } func InitLogger(config *config.Config) error { // logger設定のコードが続く traceHandler := NewTraceHandler(baseHandler, config.GcpProject) logger := slog.New(traceHandler) // logger設定のコードが続く } こちらはloggerを設定しているコードです。今回実装したシステムでは構造化ロギングパッケージとして log/slog を利用しています。 Handle メソッドはslog.HandlerのHandlerメソッドをオーバーライドしています。 その実装でログレコードに必ず logging.googleapis.com/trace 、 logging.googleapis.com/spanId 、 logging.googleapis.com/trace_sampled が設定されるようになっています。 SpanContextFromContext でctxから現在のトレース情報を取得して各フィールドに適切なフォーマットで値を指定しています。こうすることで、現在のトレース情報とログを紐づけることができ、Cloud Traceの画面上で可視化できるようになります。 Cloud Trace上でのトレース情報の見え方 以上の設定をするとCloud Traceに処理を可視化できます。 ここで、用語の説明をすると、画面上に表示される1つのプロセス全体を「トレース」と呼び、その中の各処理を「スパン」と呼びます。 一番上の /sample_service と記載されているスパンがCloud Runのロードバランサーが生成したスパンです。 http-request がCloud Run内の処理の起点となるスパンでその下にアプリケーションで実行された各処理が可視化されています。 このトレース情報を見るとCloud Runがリクエストを受け取った後に数回のクエリを実行して、Pub/Subメッセージをパブリッシュしていることがわかります。また、各スパンをクリックするとスパンの詳細情報を確認できます。 補足ですが、Cloud Runは一定レートでサンプリングを行いトレース情報を生成しているため、全てのリクエストのトレース情報を見ることは出来ません。 これから、アプリケーション上で利用されている各サービス毎の情報について説明したいと思います。 AlloyDB gorm.XXX と記載されているのがアプリケーション上から実行されたAlloyDBへのクエリを表すスパンです。 スパン詳細情報を確認すると、トレース情報からわかるクエリ実行時間の他に rows_affected 、クエリステートメントなど処理を理解するために有用な情報を確認できます。これらの情報は go-gorm/opentelemetry が自動で設定してくれるものです。 また、AlloyDBにはQuery Insightsという機能があります。この機能では、AlloyDBクラスター毎に遅いクエリの検索や、クエリの実行計画の確認などができます。Query Insightsから確認できるクエリ情報とトレース情報が紐づいていて、Cloud Trace画面へとリンクされています。そのため、Query Insightsで特定できた遅いクエリがどのアプリケーションで実行されているのかを簡単に把握できます。 Pub/Sub こちらがPub/Subパブリッシュ処理を表すスパンです。 sample-topic create のsample-topicがトピック名になるため、どのトピックに対してパブリッシュしたかを確認できます。スパン詳細情報を確認するとメッセージのBodyは確認できないもののBodyサイズなどは確認できます。トレース詳細画面からPub/Subのflow controlが行われ、その後にbatchパブリッシュが行われていることがわかります。 Cloud Logging Cloud Loggingのログ情報はトレース詳細情報に白丸として確認できます。対象のログイベントがスパン上にプロットされているため処理中のログの実行タイミングを把握できます。 クリックすると発行されたログを一覧で確認できます。 パフォーマンスチューニング 以上のようにCloud Traceにトレース情報を可視化できました。次にCloud Traceを利用して実際に実施したパフォーマンスチューニングをいくつか説明したいと思います。 データベースへの最大接続数の調整 アプリケーションに本番相当のデータを流したところ想定よりも性能が出ませんでした。 そこでトレース情報を確認したところ、以下の画像のように各クエリの実行間隔が空いていました。 各クエリ実行の間に重い処理が無かったため、DBへのコネクション待ちが発生していることがわかりました。Cloud Runの設定を確認したところ、Cloud Runの最大同時実行数よりもデータベースへの最大接続数が少なく設定されていました。つまり、アプリケーション側から作成できる接続数をクエリ発行数が上回ってしまい、コネクション待ちが発生していたという状況でした。そのため、データベースへの最大接続数を上げることでコネクション待ちを解消できました。 以下は修正後のトレース情報です。クエリが間隔を空けずに実行されていることがわかります。 クエリ実行間隔については処理を可視化しなければ気づけなかったため、Cloud Traceを利用したメリットがありました。 Pub/Subメッセージパブリッシュ数の削減 トレース情報を確認したところPub/Subメッセージのパブリッシュに想定よりも時間がかかっていることがわかりました。かつデータを1件毎パブリッシュしていて、1回のプロセスにおけるパブリッシュ回数が多く、全体の処理時間が増加していました。 そこで、今まで1つずつパブリッシュしていたデータを配列で1つにまとめ、パブリッシュ数を減らしました。メッセージ1件のサイズは大きくなったものの、パブリッシュの実行時間はほぼ変わらなかったため全体の実行時間を大きく改善できました。 クエリ実行回数の削減 トレース情報を確認したところ、以下のように単独のクエリ実行時間は短かったものの、その実行回数が多く全体の処理時間を増加させていました。 そこで、アプリケーションコードとクエリを修正することでクエリ実行回数を減らしました。以下の画像からわかるようにクエリ自体の実行時間は増えましたが、実行回数を減らしたメリットが大きく、全体の実行時間を改善できました。 「Pub/Subメッセージパブリッシュ数の削減」「クエリ実行回数の削減」に関してはある程度予想できましたが、トレース情報を確認することでより確信を持つことができました。 まとめ 本記事では、Google CloudのCloud Traceを活用してアプリケーションパフォーマンスを可視化し、パフォーマンスチューニングに取り組んだ事例をご紹介しました。Cloud Traceでデータを可視化することで、対象アプリケーションの実際の処理を理解でき、改善ポイントをより素早く特定できました。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ブランドソリューション開発本部FAANS部でAndroidアプリを担当している田中です。本記事ではバグ件数削減の施策の1つとしてFAANS Androidで実施したJetpack ComposeのUIテストの自動化についてご紹介します。 目次 はじめに 目次 背景 Firebase Test Labについて 料金について UIテストを記載する build.gradleの設定 UIテストで使用するテストファイル 1. 特定の文字列が表示されているかのテスト 2. 特定のコンポーネントが表示されているかのテスト 3. アイコン押下で意図したダイアログが表示されているかのテスト GitHub ActionsでFirebase Test Labを実行する 1.Google CloudのAPIの有効化 2.サービスアカウントとCloud Storageバケットの作成 Cloud Storageバケットの種類の選定 サービスアカウントの作成 3.GitHub ActionsからGoogle Cloudへの認証 マルチモジュールプロジェクトでFirebase Test Labを実行する (1)差分モジュールを検出 (2)変更のあったモジュールごとにテストAPK作成 (3)GitHub Actionsで利用できるように一時的にアップロードする (4)Google Cloud SDKのセットアップ (5)テストAPKごとのFirebase Test Lab実行 UIテストを導入してみて テストケースの作成状況 導入後のQAにおける不具合の検出件数について 終わりに 背景 FAANSでは、リリースまでに下記のようなフローを踏んでいます。 昨年、変更行数は約6万行で開発期間が約半年の比較的大きなリリースを伴う案件がありました。品質保証を担当するQAチームによるテスト実施において、Androidアプリは166件という非常に多くの不具合の指摘を受けてしまいました。 FAANSでは、各案件のリリース後、QAチームが品質管理の観点から開発プロセスの改善点を見出す品質報告会を実施しています。その品質報告会で当案件について、Androidで検知された不具合のうち約8割がQA実施フェーズに入る前の段階で検知可能であると共有されました。 Androidチームでも不具合を分析してみると、たしかにその多くが仕様に対する認識漏れや実装漏れといった開発フェーズで対処できる軽微な内容でした。 このような軽微なバグがQA実施フェーズまで流れ込むのを防ぐため、Androidチームでは不具合を検知・防止するアプローチの1つとしてUIテストを導入することにしました。 本記事では、Firebase Test Labを活用してGitHub Actions上でUIテストを自動化した方法と、その運用状況について紹介します。 Firebase Test Labについて 以前弊社が公開した Firebase Test Labを使ったAndroidアプリのテスト に詳しい内容が記載されているのでご参照ください。 Firebase Test Labは、GoogleのFirebaseが提供するクラウドベースのアプリテストサービスです。多様な実機や仮想デバイス上で自動化されたテストを実行し、アプリの動作やパフォーマンスを検証できます。 本記事では、UIテストを実行するためにFirebase Test Labで実行できる Instrumentation Test について触れます。Firebase Test Labを活用することで、GitHub Actionsを用いた自動テストにUIテストを組み込むことが可能になります。 料金について Test Labの利用料金は、1日あたりのテスト実行数や実行時間で算出されます。 FAANS AndroidはFirebaseのBlazeプランを利用しており、また、Firebase Test Labでは仮想デバイスを使用するテストを選択しました。その場合の料金体系は以下のとおりです。 無料枠: 1日あたり60分のテスト時間 超過時: 各仮想デバイスにつき1時間あたり$1 料金の詳細はTest Lab公式ドキュメントの 割り当て をご覧ください。 UIテストを記載する 本記事ではFirebase Test Labの運用についての内容を主軸としているためUIテストについての説明は比重を低くしています。 UIテストを記載、実行するために以下を設定します。 build.gradleの設定 dependencies { androidTestImplementation "androidx.test:runner: $androidXTestVersion " androidTestImplementation "androidx.test:rules: $androidXTestVersion " androidTestImplementation "androidx.compose.ui:ui-test-junit4: $compose_version " debugImplementation( "androidx.compose.ui:ui-test-manifest: $compose_version " ) } android { defaultConfig { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } } 詳細な設定方法については、 Instrumented Testsの設定 および Compose レイアウトをテストする をご参照ください。 UIテストで使用するテストファイル デフォルトで作成される ExampleInstrumentedTest.kt があれば、最低限テストを実行できます。このファイルは module-name/src/androidTest/java/ に配置されます。 /** * Instrumented test, which will execute on an Android device. * * See [testing documentation](http://d.android.com/tools/testing). */ @RunWith (AndroidJUnit4 :: class ) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertEquals( "jp.android.faans.base.test" , appContext.packageName) } } FAANS AndroidではFragmentごとにテストを作成しています。実際に記載しているテストケースを3つ紹介します。 1. 特定の文字列が表示されているかのテスト このテストでは、投稿された動画一覧の画面で投稿が0件の場合に、特定の文言が表示されるかを確認します。 onNodeWithText は特定の文字列が画面に表示されている場合に検知でき、 assertIsDisplayed() で表示されているかを検証できます。 @Test fun when_video_items_is_empty_then_display_enroll_video_nothing_text() { composeTestRule.setContent { FaansTheme { RegisterRelatedVideoScreen( onNavigationIconPressed = {}, dispatchAction = {}, state = fakeSuccessState.copy(videoItems = emptyList()), initialSelectedVideoItems = emptyList(), onEnrollButtonClicked = {}, ) } } composeTestRule.onNodeWithText( "ノウハウ動画は \n まだ投稿されていません" ).assertIsDisplayed() } 2. 特定のコンポーネントが表示されているかのテスト こちらのテストではUIを構成するStateがLoadingの状態の時にローディングのコンポーネントが表示されているかをテストしています。 onNodeWithTag は、 Modifier.testTag で設定したタグ名のコンポーネントを検知でき、1と同様に assertIsDisplayed() で表示されているかを検証できます。 @Test fun when_state_is_loading_then_ui_is_loading() { composeTestRule.setContent { FaansTheme { RegisterCoordinateTagScreen( onNavigationIconPressed = {}, dispatchAction = {}, state = RegisterCoordinateTagState.Loading, onEnrollButtonClicked = {}, ) } } composeTestRule.onNodeWithTag( "progress" ).assertIsDisplayed() } 3. アイコン押下で意図したダイアログが表示されているかのテスト このテストでは、特定のアイコンを押下した後に表示されるボタンを選択した際、意図したダイアログが正しく表示されるかを確認します。 assertIsDisplayed() を使った検証は上記2つと同様で、 performClick() を利用することで特定のコンポーネントを押下できます。 @Test fun when_video_delete_menu_tap_then_is_display_video_delete_dialog() { composeTestRule.setContent { FaansTheme { CoordinateDetailScreen( page = 0 , state = fakeCoordinateState.copy( isTransitionFromRanking = false , wayDetail = fakeWayDetail, ), actionDispatcher = { }, onVolumeChanged = { }, onPopBackStack = { }, onTransitionToReviewComments = { _, _, _ -> }, onTransitionToEditPage = { _, _ -> }, onTransitionToBreakDownDetail = { _, _ -> }, onNavigateWearPreview = {}, onNavigateVideoRegistration = {}, ) } } composeTestRule.onNodeWithTag( "know_how_menu_icon" ).performClick() composeTestRule.onNodeWithText( "投稿を削除する" ).performClick() composeTestRule.onNodeWithText( "削除の確認" ).assertIsDisplayed() composeTestRule.onNodeWithText( "ノウハウ動画を削除しますがよろしいですか?" ).assertIsDisplayed() composeTestRule.onNodeWithText( "削除する" ).assertIsDisplayed() composeTestRule.onNodeWithText( "キャンセル" ).assertIsDisplayed() } 他にも performScrollTo() を利用した画面のスクロール操作や performTextInput() を利用した文字入力によるUIのテストなども作成しています。 Jetpack ComposeのUIテストで可能なことは、公式ドキュメントの テスト早見表 にまとめられているのでご参照ください。 GitHub ActionsでFirebase Test Labを実行する Firebase Test Labを使ったUIテストを自動化させるにあたり、 CIシステムでテストする ではJenkins CIでの実行について記載があります。 FAANS AndroidではCIシステムをGitHub Actionsで行なっているため、 要件 に記載がある手順を自チームの環境に適応させて以下の流れで導入しました。 Google CloudのAPIの有効化 サービスアカウントとCloud Storageバケットの作成 GitHub ActionsからGoogle Cloudへの認証 こちらの手順について、FAANS Androidで実施した内容を説明します。 1.Google CloudのAPIの有効化 要件 に記載がある通り、 Google Cloud Testing API と Cloud Tool Results API を有効にします。 2.サービスアカウントとCloud Storageバケットの作成 Cloud Storageバケットの種類の選定 要件 には、Test Labで使用するサービスアカウントに関して次のような記載があります。 Create a service account with an Editor role in the Google Cloud console and then activate it. 「Editor role」とは、Google CloudのIAMの基本ロールの1つである編集者ロール( roles/editor )を指しています。 このロールはGoogle Cloudプロジェクト内の全てのプロダクトに跨る大量のIAM権限を有した強い権限を持つため、サービスアカウントへの使用はセキュリティの観点から一般的には推奨されません。 そのため、編集者ロールを付与したサービスアカウントの使用はできれば避けたいと考えました。 調査したところ、Test Labで生成されたテスト結果保存先のCloud Storageバケットとして、以下の2種類から選択できることが分かりました。 構築・管理不要なFirebaseが自動管理するデフォルトバケット 自前で構築・管理する独自バケット 独自バケットを利用すると、編集者ロールよりもずっと少ないIAM権限(※後述)で済みます。ただし、デフォルトバケットはテスト結果の保持期間に90日という制約がある一方、ストレージにかかるコストが無料である点にメリットがあります。 FAANSでは、Test Lab上で想定されるテストの実行頻度や保存期間を基に、独自バケットのストレージコストを見積もりました。その結果、独自バケットの場合に上乗せされるCloud Storageのコストとセキュリティ強度のトレードオフを踏まえ、独自バケットを使用することに決めました。以降は独自バケットを使用する前提の説明になります。 以下は、独自バケットを作成するTerraform定義の例です。 resource "google_storage_bucket" "firebase_test_lab" { name = "<バケット名>" location = "us-central1" uniform_bucket_level_access = true # Firebase Test Labのテスト結果を過去7日分保持する場合のライクサイクル設定 lifecycle_rule { action { type = "Delete" } condition { age = 7 } } } gcloud CLIでTest Lab実行時、独自バケットへテスト保存は「 gcloud firebase test android run 」の --results-bucket で指定可能です。 サービスアカウントの作成 独自バケットを利用する場合、Firebase Test Lab用サービスアカウントに以下2つの事前定義ロールを付与する必要があります。 Firebase Test Lab Admin( roles/cloudtestservice.testAdmin ) Firebase Analytics Viewer( roles/firebase.analyticsViewer ) 詳細な権限設定については公式ドキュメントの IAM 権限リファレンス ガイド を参照してください。 サービスアカウントと、必要なIAMロールを付与するためのTerraform定義例は以下の通りです。 resource "google_service_account" "firebase_test_lab" { account_id = "firebase-test-lab" } resource "google_project_iam_member" "firebase_test_lab" { for_each = toset ( [ "roles/firebase.analyticsViewer" , "roles/cloudtestservice.testAdmin" , ] ) project = <Google CloudのプロジェクトID> role = each.key member = "serviceAccount:$ { google_service_account.firebase_test_lab.email } " } 次の手順で行う「GitHub ActionsからGoogle Cloudへの認証」にて上記で作成したサービスアカウントを利用します。 3.GitHub ActionsからGoogle Cloudへの認証 gcloud CLIでテストを開始する ではGoogle Cloud SDKをダウンロードしてgcloud CLIにログインするといった手順が記載されています。GitHub Actionsの場合は以下のyaml定義で、Google Cloudへの認証とCloud SDKのインストールをCIの処理で実行できます。 - name : Authenticate to Google Cloud uses : google-github-actions/auth@v0.4.0 with : # 上記で作成したサービスアカウントと連携済みのWorkload Identityプロバイダーを指定。 workload_identity_provider : 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' # 上記で作成したサービスアカウントを指定。 service_account : 'my-service-account@my-project.iam.gserviceaccount.com' - name : Set up Google Cloud SDK uses : google-github-actions/setup-gcloud@v2 Google Cloudへの認証をGitHub Actionsで行う際は以下のアクションを利用できます。 google-github-actions/auth@v0.4.0 google-github-actions/setup-gcloud@v2 以上でFirebase Test Labを使用する前準備が完了します。 マルチモジュールプロジェクトでFirebase Test Labを実行する FAANS Androidは複数のGradleモジュールが存在するマルチモジュールプロジェクトです。 Firebase Test LabのInstrumentation Testは、モジュールごとに実行されます。 マルチモジュールプロジェクトの場合、全体をテストするには1度のCIの実行で全モジュールのテストを実施する必要があります。 しかし、毎回全モジュールをテストすると膨大なテストが走ってしまうため、テスト実行回数を抑制することにしました。 そのため、FAANS Androidでは差分のあるモジュールのみを対象にテストを実施しています。 以下の流れで実際にテスト実行回数を制御しています。 (1)差分モジュールを検出 次のコードを1つのステップで実施します。 1.ベースブランチのフェッチ # Fetch the base branch echo "Fetching the base branch: refs/heads/$BASE_REF" git fetch origin refs/heads/$BASE_REF || git fetch --all 2.プロジェクトの全モジュール名を格納 # Get the list of modules echo "Getting the list of modules..." modules=$(./gradlew projects --quiet | grep -oP "^.*--- Project ':\K[^\']+" ) modules_list=$(echo "$modules" | sed 's/:/\//g' | tr '\n' ' ' | sed 's/[[:space:]]*$//' ) ここでは feature/mypage といったモジュールがあったときに、 featureとfeature/mypage が取得されます。 親の階層の feature 自体は不要なので後続のステップで取り除く形としています。 3.ベースブランチと作成中のブランチの差分ファイルを検出 # Determine changed files echo "Getting the list of changed files..." changed_files=$(git diff --name-only origin/$BASE_REF $GITHUB_SHA) echo "Changed files:" echo "$changed_files" | tr ' ' '\n' 4.差分ファイルがあるモジュールの割り出し # Identify changed modules echo "Identifying changed modules..." changed_modules="" for module in $modules_list; do if echo "$changed_files" | grep -q "^$module/" ; then changed_modules="$changed_modules $module" fi done 5.余分な空白の削除 # Clean up leading/trailing spaces changed_modules=$(echo $changed_modules | sed 's/^ *//' ) echo "Changed modules detected: $changed_modules" 6.差分モジュールが存在するかの確認 # Set environment variable or skip tests if [ -z "$changed_modules" ] ; then echo "No changed modules detected. Skipping instrumentation_test." echo "SKIP_TESTS=true" >> $GITHUB_ENV else echo "Tests are required for changed modules: $changed_modules" echo "changed_modules=$changed_modules" >> $GITHUB_ENV echo "SKIP_TESTS=false" >> $GITHUB_ENV fi SKIP_TESTS フラグでテスト実施不要な場合の制御を行なっています。 changed modules には、 base domain domain/domain のようなモジュール名が格納されます。 (2)変更のあったモジュールごとにテストAPK作成 変更があったモジュール毎にassembleDebugAndroidTestを実行してテストAPKをそれぞれ作成します。 - name : Build test APK if : env.SKIP_TESTS == 'false' run : | for module in ${{ env.changed_modules }}; do echo "Building test APK for module $module" if [[ $module == */* ]] ; then ./gradlew :${module//\//:}:assembleDebugAndroidTest || true else ./gradlew :$module:assembleDebugAndroidTest || true fi done ${module//\//:} は、モジュール名中のすべての / を : に置き換える処理 :app:module:assembleDebugAndroidTest の形でテストを行う必要があるため、 app/module を app:module の形に変換する || true を設定しているのはモジュールの階層の親でテストが存在しないモジュールも含まれてしまっているため、そのような場合のテストが失敗した時にテストが止まらないようにするため (3)GitHub Actionsで利用できるように一時的にアップロードする 各モジュールのテストAPKは以下に格納されます。 /$module/build/outputs/apk/androidTest/debug/*-debug-androidTest.apk それぞれのモジュールに配置されたテストAPKを利用しやすい形にするため、GitHub Actions上の ./test-apks というパスに作成されたテストAPKを配置するようにします。 - name : Archive assembled test APKs if : env.SKIP_TESTS == 'false' run : | mkdir -p ./test-apks for module in ${{ env.changed_modules }}; do apk_path=$(find . -type f -path "*/$module/build/outputs/apk/androidTest/debug/*-debug-androidTest.apk" ) if [ -n "$apk_path" ] ; then echo "Found test APK for module $module: $apk_path" cp "$apk_path" ./test-apks/ else echo "No test APK found for module $module" fi done - name : Check if test-apks directory has files if : env.SKIP_TESTS == 'false' run : | if [ -z "$(ls -A ./test-apks)" ] ; then echo "No test APKs found. Setting SKIP_TESTS=true." echo "SKIP_TESTS=true" >> $GITHUB_ENV fi - name : Upload test APKs if : env.SKIP_TESTS == 'false' uses : actions/upload-artifact@v4 with : name : assembled-test-apks path : ./test-apks (4)Google Cloud SDKのセットアップ GitHub ActionsでFirebase Test Labを実行する セクションの「3.GitHub ActionsからGoogle Cloudへの認証」に記載した設定方法を実行します。 (5)テストAPKごとのFirebase Test Lab実行 ./test-apks に格納されているテストAPKごとにInstrumentation Testを実行することで、差分があるモジュールの数だけFirebase Test Labが実行されます。 - name : Run Firebase Test Lab if : env.SKIP_TESTS == 'false' run : | for test_apk in ./test-apks/*.apk; do gcloud firebase test android run \ --type instrumentation \ --app app/build/outputs/apk/debug/app-debug.apk \ --test "$test_apk" \ --results-bucket=<作成したCloud Storageの独自バケットの名前> \ --device model=lynx,version=33,locale=ja_JP,orientation=portrait done 以上の設定を含むGitHub Actionsのワークフローファイルをリポジトリに配置することで、GitHub ActionsからFirebase Test Labを利用した自動テストを実行できます。 UIテストを導入してみて テストケースの作成状況 現在のテストケース作成状況は以下のとおりです。 テストが存在しているモジュールは18個 プロジェクト全体で作成済みのテストケースは154個 FAANS Androidで最もテストケースを作成しているモジュールには現状79個のテストケースが存在します。 こちらのモジュールでテストを実施した際、Firebase Test Labの実行時間は40秒ほどでした。 差分を検出したモジュールが1つの場合一連のテスト実行時間は大体20分程度になっています。FAANS Androidで別途、並列実施しているCIと実行時間があまり変わらないため、現状は開発の生産性を落とすようなテストとはなっていません。 また、CI上でGoogle Cloudへの認証から、APKのビルド、そして全モジュールに対するInstrumentation Testの実施完了までにかかる合計時間は約40分となります。 (なお、この時間はFirebase Test Labの従量課金で計算される実行時間とは異なります) 1つのプルリクエストで全てのモジュールにコード差分が発生することは基本的にはないので目安程度の数字です。 導入後のQAにおける不具合の検出件数について UIテスト導入前の開発案件(変更行数は約6万行、開発期間は約半年)では、166件の不具合検出がありました。 一方、UIテストを実装した案件(変更行数は約6万行、開発期間は約4か月)では、不具合検出が23件に減少しました。 また、QAチームからの品質報告会でもQA実施時に検出された不具合の内、QA実施前に検出されるべき不具合の件数が非常に下がったと共有を受けることができました。 UIテスト導入は、実装者とレビュアーが満たすべき仕様を正確かつ効率的に把握しやすくなった点やリグレッションを検知しやすくなった点で、品質向上に一定の効果が得られたと感じています。 終わりに 本記事ではFAANS Androidチームにおける、UIテストの自動化と現状のテスト運用について紹介しました。 UIテストの導入により、実装者とレビュアーは仕様誤認や実装漏れを防ぎやすくなり、開発プロセスの品質向上に大きく繋がりました。 しかし、現状は差分があるモジュールであれば実行対象になってしまうので明らかにUIに影響がないような差分でもテストが実行されてしまうなど、まだまだ改善の余地があります。 また、UIテストについても重要度が高いテスト内容とは何かなど、テストの質についても日々チームで議論を重ねています。 今後もチームとしてさらに品質向上に努めていければと思います。 Firebase Test Labの導入を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、ZOZOMO部の中島です。普段は Fulfillment by ZOZO や ZOZOMO店舗在庫取り置き というサービスの開発を担当しています。 2025年1月12日から14日の3日間にかけてニューヨークで開催された「NRF 2025: Retail's Big Show」に現地参加してきました。私個人としては、昨年に引き続き2回目の参加になります。 前半はNRF Retail's Big Showの概要と関連する情報、後半はセッションの内容やExpoで気になったものを中心にお伝えします。NRF 2025全体の概要については、 NRF 2025 Event Recap などをご覧ください。 目次 はじめに 目次 NRF Retail's Big Showとは 会場の概要 セッション オープニングセッション NRF 2025イベントの紹介 小売業におけるAIの活用事例 Physical AIとロボット訓練 キャリアに関するアドバイスと業界の展望 オープニングセッションまとめ Expo 生成AI 気になったデバイス ORA Sphere iRomaScents あなたはどのGame Changer? おわりに NRF Retail's Big Showとは NRF会場のエントランス NRF Retail's Big Show は、毎年1月にNRF(National Retail Federation)が主催する、世界最大級の小売業界向け展示会です。2025年は1月12日から14日までニューヨークで開催され、175のセッションが行われ、1,000社以上の企業が出展しました。 毎年、NRF Retail's Big Showにはテーマが設定されており、2025年のテーマは「GAME CHANGER」でした。 今年のテーマ:GAME CHANGER 今回も、公式サイトではおすすめホテルの紹介や、イベントをより快適に楽しむための「NRF 2025アプリ」が提供されました。私自身もアプリを活用し、事前に見たいセッションを登録しました。当日はスケジュール管理やセッション開始時間のチェックに役立てました。 NRF 2025アプリ 会場の概要 NRF 2025は、2024年と同様に Jacob K. Javits Convention Center で開催されました。 Jacob K. Javits Convention Centerの外観 2階がメインエントランスになっており、カンファレンスパス購入時に発行される2次元コードを使って受付をします。 NRF 2025の受付 NRF 2025に参加するためのカンファレンスパスは、All-Access PassとExpo Passの2種類があります。Expo PassだとExpo会場のみ参加可能で、Keynoteセッションなどが開催される会場に入るには、All-Access Passが必要になります。 前回は、All-Access Passの値段が時期によっては結構差がありましたが、今回はあまり時期による変動が大きくなかったです。ただ、早ければ早いほど安く購入できますので、次回参加する予定の方は、夏くらいから動き始めると最安で購入できると思います。ちなみに、受付後にバッジをもらいますが、そのバッジを紛失すると再発行にカンファレンスパス購入時と同じ金額が必要になります。そのため、受付のときに絶対なくさないように念押しされてバッジを渡されました。 早朝はDonuts Dunkというものがあり、ドーナツなど軽食を食べながらネットワーキングできる時間が設けられています。今年も数社、日本企業の方とコミュニケーションを取ることができました。 Donuts Dunkで提供されるドーナツや果物 セッション NRFでのセッションは、Javits Centerの1階、3階、4階、5階の各会場で実施されます。参加パスの種類によってアクセスできるセッションが異なります。All-Access Passを持っていればすべてのセッションを視聴できますが、Expo Passの場合はExpoが行われる1階と3階のセッションのみに限られます。 5階フロア全体を使用するSAP Theatreステージは、最も大きな会場となっており、ここでKeynoteセッションが開催されます。各ステージにはスポンサー名が付いており、前回と名称が変わらなかったため、毎年同じスポンサーがついているのかもしれないです。 オープニングセッション オープニングセッションでは、Walmart U.S.のPresident and CEOであるJohn Furner氏と、NVIDIAのVice President and General Manager, Retail & CPGであるAzita Martin氏の対談が行われました。NRFは小売業向けの展示会ですので、例年だと小売企業の幹部がオープニングセッションに登壇していました。今回はNVIDIAが登壇したことからも、AIに対する業界の関心が非常に高まっていることがうかがえます。 このセッションでは、以下のようなテーマが取り上げられました。 NRF 2025イベントの紹介 小売業におけるAIの活用事例 Physical AIとロボット訓練 キャリアに関するアドバイスと業界の展望 それぞれの内容について、以下で詳しく紹介します。 NRF 2025イベントの紹介 セッションの冒頭では、NRFイベントの紹介が行われました。NRFは1911年に始まり、114年の歴史を誇るイベントであり、現在では40,000人の参加者、6,200のブランド、450人のスピーカー、175のセッションを擁する大規模イベントへと成長しています。また、ニューヨーク市には約7,500万ドルの経済的影響をもたらしているとのことです。さらに、小売業界の成長率についても言及され、2024年の成長率は2.5%から3.5%の範囲で予測されています。 小売業におけるAIの活用事例 小売業界におけるAIの導入事例として、以下の企業の取り組みが紹介されました。 L'Oreal:マーケティングの分野で生成AIを活用し、コンテンツ制作や顧客対応を強化。 Walmart:膨大なデータを処理し、SKUと店舗の何億もの組み合わせを毎週予測。需要予測の精度向上に貢献。 Lowe's:1,700店舗でデジタルツインを導入し、店舗レイアウトの最適化と業務効率化を推進。 これらの事例からも、小売業界におけるAI活用の広がりと、データ分析やシミュレーション技術の進化がうかがえます。 Keynoteで紹介されたAI活用の動画はこちらになります。 www.youtube.com Physical AIとロボット訓練 次に、Physical AIの概念とその応用について紹介がありました。Physical AIについて初めて聞いたため調べたところ、 NVIDIAの公式サイト に説明がありました。要約すると、「現実世界の空間や物理法則を認識・理解し、適切な判断を下しながら自律的に行動できるAI」と定義されています。 Keynoteの紹介では、倉庫や配送センターのデジタルツインを作成し、Physical AIを活用することで、現実世界では想定できない数多くのシナリオをシミュレーションできるようになるとのことでした。物理法則を考慮したシミュレーションデータを利用することで、ロボットの学習が効率的になり、より高度なAIロボットの開発が期待されます。 この技術によって、物流・倉庫業務におけるAIの応用範囲が拡大し、業務効率の向上とコスト削減の可能性があります。特に、デジタルツイン技術と組み合わせることで、よりリアルなシミュレーションが可能になり、試行錯誤のコストを削減しながらロボットの最適化が実現できます。 キャリアに関するアドバイスと業界の展望 セッションの最後には、キャリア開発に関するアドバイスが語られました。特に以下の点が強調されました。 AI技術の活用が今後ますます重要になること 継続的な学習の必要性 キャリア開発におけるリスクを計画的に取ること これからの小売業界では、AI技術が不可欠な要素となるため、企業だけでなく個人のスキルアップも求められる時代になっていくでしょう。 オープニングセッションまとめ NRF 2025のオープニングセッションでは、小売業界におけるAIの活用がいかに重要であるかが強調されました。WalmartやL'Oreal、Lowe'sなどの企業がすでにAIを活用して成果を上げていることからも、この分野の進化が加速していることが分かります。また、Physical AIとロボット訓練の進展により、物流業界における自動化がさらに進んでいくかもしれません。 AIが小売業界に与える影響は今後も大きくなると予想されます。これに伴い、業界関係者はAI技術への理解を深め、積極的に活用していくことが求められそうです。 Expo Javits Center内の1階と3階が主なExpo会場になっていました。Expo会場は、多くの企業がブースを出展し、最新のテクノロジーやサービスを紹介しています。また、各ブースではデモンストレーションやプレゼンテーションが行われており、訪れた参加者に直接情報を提供していました。 1階、3階、RIVER PAVILIONのフロアマップ ブースの中でも、RIVER PAVILION内の「NRF 2025 Innovators Showcase」エリアや、1階の「Startup Hub」エリアに多くのスタートアップ企業が集まっています。ブースの回り方として、私はまず最新の技術やトレンドを確認するためにこれらのエリアを巡り、その後、3階にある大手企業のブースを訪れる流れでExpoを回りました。 生成AI 前回NRFに参加した際は、生成AIについて取り組み始めているという温度感だったものが、今回のNRFでは生成AIに関連しないものを探すほうが難しいくらいAIに溢れていました。また、生成AIに関してはスタートアップより、大手企業の方が網羅的にカバーしている印象が強かったです。 AWS展示、左からDiscover、Find、Purchase、Post-Purchaseが表現されている 上記はAWSのブース展示です。Discover、Find、Purchase、Post-Purchaseというカスタマージャーニーがモデルで表現されています。このカスタマージャーニーに沿ったパーソナライゼーションをAWSのサービスを使って提供する仕組みについて紹介されていました。 最初に、対象の製品とブランドやキャンペーンのテーマを決めると、自動的に、各フェーズで顧客を引き付けるための推奨事項を提供できるとのことでした。 具体的には、ユーザーごとにパーソナライズされた画像やメールのコピー文の作成や、購入商品に合わせたレコメンデーション、バーチャルアシスタントでの購入後のユーザーサポートなどができるとのことです。 左:3Dホログラムでの商品表示、右:AIアシスタントが商品説明 他にも、以前見たことがあった Proto という製品も進化していました。大きな3Dホログラムのディスプレイというだけではなく、生成AIを活用して商品の説明を音声で提供する機能が追加されていました。さらに、英語だけでなく日本語にも対応しており、言語の壁を超えて情報を伝えられる点は、生成AIならではの進化だと感じました。 気になったデバイス Startup Hubのブースを回っている中で、特に印象に残ったデバイスを2つ紹介します。 ORA Sphere まず1つ目は、球体のタッチ操作が可能なディスプレイ ORA Sphere です。 このデバイスの特徴は、360度に映像を投影できるだけでなく、タッチ操作も可能な点です。球体ディスプレイ自体が珍しいですが、触れて操作できるのは新しい体験でした。主な用途としては、地球や惑星の表示などが想定されているようです。球体×ディスプレイ×タッチ操作という組み合わせがユニークで、今後の活用シーンに期待したいです。 iRomaScents 次に紹介するのは、 iRomaScents という香りを提供するデバイスです。 このデバイスには45種類の香りをセット可能で、ユーザーが選択した香りをサンプルとして提供できます。さらに、ウィザード形式で質問に答えることで、ユーザーに最適な香水をレコメンドする機能も備わっていました。 香りのパーソナライズが進化することで、店舗やECでの香水選びの体験が変わる可能性を感じました。 あなたはどのGame Changer? イベント会場を回っていると、American Expressのブースで「あなたはどのGame Changer?」という診断ゲームが実施されていたので、試してみました。性格診断のような質問に答えていくと、結果はTech Expertでした。 ちなみに、診断では以下のようなタイプに分類されるようです。 Operations Ace Tech Expert Marketing Genius Digital Professional Gen AI Specialist 「Gen AI Specialist」が診断結果に含まれることが、今回のNRFの特徴だと感じました。 American ExpressのGame Changer診断 おわりに ホテルの近くにあったエンパイア・ステート・ビル 今年のテーマが「GAME CHANGER」だったため、昨年と比べてどのような変化があるのかを意識しながら参加しました。 2024年、2025年と2年連続で参加しましたが、昨年は一部の企業が生成AIを使い始めた段階だったのに対し、今年はほぼすべての企業が生成AIを活用し、その成果も見え始めている印象を受けました。ただし、生成AIによる劇的な変化はまだ先の段階と感じており、来年以降の進化に期待しています。 今回のNRF視察は、開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 NRFは技術カンファレンスではありませんが、小売業におけるテクノロジー活用の最新事例を学べる貴重な機会です。技術者としてこうしたイベントに参加することで得られるものは大きいと感じています。また、NRF Retail’s Big Showはこれまでのニューヨークとシンガポールに加え、新たにパリでの開催も発表されていましたので、ご興味のある方はぜひ参加してみてください。 ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 最後までご覧いただき、ありがとうございました!
はじめに こんにちは。Developer Engagementブロックの @wiroha です。2月14日に「 ZOZO Tech Meetup ~データサイエンス~ 」を開催しました。ZOZOTOWNを支える開発において「データサイエンス」にフォーカスして、弊社データサイエンティストが具体的な事例を交えながら紹介するオフラインイベントです。 登壇内容まとめ ビジネスアナリティクス部から次の3名が登壇しました。 発表タイトル 登壇者 因果推論が浸透した組織の現状と未来 マーケティングサイエンスブロック 茅原 難題に挑むデータアナリティクス:意思決定を支える分析の舞台裏 データサイエンスブロック 橘 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話 マーケティングサイエンスブロック 佐々木 因果推論が浸透した組織の現状と未来 マーケティングサイエンスブロック 茅原による発表 speakerdeck.com 茅原からは因果推論(因果関係を統計的に推論すること)が浸透しているZOZOにおいてどのような課題が生じているか、またその対策について発表しました。部門内外でのナレッジシェアが活発に行われているのを感じました。 難題に挑むデータアナリティクス:意思決定を支える分析の舞台裏 データサイエンスブロック 橘による発表 speakerdeck.com 橘の発表では、難題を「技術的新規性×事業的複雑性」と定義し、これまでの事例を分類して解説しました。難易度の高い案件の解決方法は参加者の参考になっていたと感じます。 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話 マーケティングサイエンスブロック 佐々木による発表 speakerdeck.com 佐々木からの発表で、分析部門が相対する事業部の社員はSQLを習得済みであると語ったところ、参加したみなさまは驚いていたようです。発表後のAsk the Speakerの時間でも「どうしたらこういう組織を作れるのか」といった話題がよく出ていました。 (2025/03/13追記)質問への回答 開催後のアンケートにて多数の質問をいただきました。この場にて回答いたします。 Q. 事業部門とのコミュニケーションをとるときに意識していること(特に分析のリテラシーが高くない場合)、各事例のプロジェクト運用についても詳細を知りたいです。大きなプロジェクトは運用も大変なのかなと予想しています。 A. 事業部門とのコミュニケーションでは特に「寄り添い」と「染み出し」を意識しています。まずビジネス部門に寄り添い、相手の真意を汲み取った上で使いやすいアウトプットを設計します。その上で、分析側がビジネス部門側へ染み出し、ビジネス部門側の課題設計や方針検討など、分析の根本の部分から並走して進めることを意識しています。また、各事例でのプロジェクト運用について、大きなプロジェクトでは積極的に小さなタスク粒度にブレイクダウンした上でのゴール設計を行っています。こうすることで、短いスパンで方針の再検討でき、大きなプロジェクトにありがちな「完了してからゴールがズレていることに気づいた」といった事例を防ぐことができます。 Q. 難題がどうかを分けるメリットについて。スケジュールやアサイン調整に利用するとか浮かんだのですが、他に案件難度を定量化する先のメリットがあれば、お伺いしたいです。 A. おっしゃる通り、スケジュールやアサイン調整に利用します。他の用途としては、半期に1度の評価の際に自分の成果を定量的に示すために使ったりします。 Q. ゆっくり配送のような難題案件を対応する際はアナリスト何名くらいで行なっているのでしょうか。また、ビジネス側なども含めてどのような業務分担で実施しているのでしょうか。 A. アナリスト側はシニア1名、レビュアー(マネージャー)1名の体制が基本です。シニアがビジネス側のMTGに参加し、困ったときはレビュアーに相談という形ですが、案件の難易度によってはレビュアーも全MTGに参加することがあります。ビジネス側は事業や施策の担当者が1~2名、開発が伴う場合は要件に合わせて各チームから担当者が付きます。大規模になると、全体を取りまとめるPMがアサインされる場合もあります。 Q. 施策の内容的に長期的影響(購買頻度の変化)とかも重要なのではと思ったりしたのですがどうでしょうか? A. おっしゃる通り長期的な影響もあるかもしれないのですが、送料無料CPについては月単位の予算を達成するためのギャップフィル施策として月に複数回実施しているため、現状は長期的な影響を気にしていません。 Q. 三つ目の発表に関して、意思決定後にKPIをフォローし上手く行かなかった際の組織的な振り返りについてどのようにされているかお伺いしたいです。 A. 「なぜうまくいかなかったのか?」について事業部とディスカッションを行っています。 想定通りの事象が起きていたがKPIは下がっていた 想定通りの事象が起きておらずKPIは下がっていた の切り分けを行ったうえで、各要因の仮説出し・検証を行います。 Q. 中途入社1年目社員が語る!ZOZOのデータ分析組織の魅力 / 意思決定の"正しさ"を測れるようにした話について、事業部の人がクエリを書けるとそれで簡単な検証ができてしまい、専門チームのバリューが少なくなりそうとおもいました。特に因果推論みたいな保守的な手法はかなりクリティカルかなと思うのでもっと聞いてみたかったです。 A. ご記載いただいている通り難度の低い検証は事業部の方でも可能ではありますが、他の部分で専門チームのバリューを発揮できると考えています。例えば、次のような部分です。 簡単な検証が可能となるような施策及びABテスト設計 検証後の「ではどうしたら良いのか?」という部分に対する示唆だし 発表でも記載している通り、「数値抽出」ではなく「意思決定への寄与」が分析チームに求められる部分のため、「専門チームのバリューが少なくなる」とはとらえていません。むしろ逆に、事業部が単独で簡単な検証を間違えずに行える環境・ルールを作り上げていくのも専門チームの役割と捉えています。全社の意思決定の加速を考えた時、分析チームがボトルネックにならない体制を作り上げていきたいです。 最後に 登壇したみなさん 本イベントでは発表中、多くの参加者が熱心にメモを取っており、高い関心が伝わってきました。発表後は参加者・登壇者がカジュアルに質問をし合いながら交流し、より深い情報や各社の悩みを共有する機会となりました。今後もこういったイベントを開催していきますので、ぜひご参加ください! ZOZOでは一緒に働く仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは。千葉県の特産品として真っ先に思い浮かぶものがヨウ素 *1 な、データシステム部データ基盤ブロックの塩崎です。 この記事ではBigQueryストレージの費用を計算する方法と、費用を節約するための戦略について説明します。BigQueryストレージの費用計算をするために、まずストレージを2軸・8種類に分類し、それぞれの軸の視点から費用節約をする方法を紹介します。特にTime travel機能やFail-safe機能が関わると計算ミスをしやすくなるため、それらについても説明します。 ストレージの分類 最初にBigQueryストレージを分類するための2つの軸を説明します。1つ目の軸はライフサイクルで、これはテーブルの更新・変更・削除等の操作によって変化するものです。2つ目の軸は課金モデルで、これは非圧縮状態のデータ量で費用を計算するか圧縮済み状態のデータ量で費用を計算するかを決めるものです。 ライフサイクルは4種類の状態、課金モデルは2種類の状態が存在するため、これらの組み合わせは2*4=8種類になります。BigQueryストレージの費用の計算ミスはこれら8種類を正しく認識できていないことに起因して起こることがしばしばあります。そのため、まずはこれら8種類の分類を正しく理解することが大事です。 ライフサイクル ここから4種類のライフサイクルの状態について説明します。 Active Current Storage 最初に説明するのはActive Current Storageです。この用語は公式ドキュメントには載っていませんがとても重要な概念です。似た用語であるActive Storageという用語が公式ドキュメントで使われている一方で、その用語の意味が文脈によってまちまちであるため、いろいろな誤解が生じています。そのため、この記事では定義が曖昧なActive Storageという用語を極力使わずにActive Current Storageという用語で説明をします。 90日以内に作成・更新されたテーブルがActive Current Storageという状態になります。テーブルをロードして作成したり、既存のテーブルにINSERT・UPDATEをすると、この状態になります。 公式ドキュメントに載っているActive Storageと本記事のActive Current Storageは似ているものの別物であることを再度強調しておきます。 Long-term Storage Active Current Storageのテーブルを90日間更新せずにおくと、Long-term Storageに自動的に移行されます。また、Long-term Storageのテーブルに対してINSERT・UPDATE・DELETE等の更新処理を行うとActive Current Storageに戻ります。 Time travel Storage Time travel Storageは削除されたデータが一時的に配置される場所です。ファイルシステムにおける「ゴミ箱」のような場所です。 DELETE文で削除されたデータだけではなく、UPDATE文で上書きされたデータもTime travel Storageに送られます。これは1つのUPDATE文をDELETE文+INSERT文が組み合わさったものだと考えればわかりやすいです。 Time travel Storageに配置されているデータは、テーブルがまだ存在する場合には、 FOR SYSTEM_TIME AS OF 構文で参照できます。 cloud.google.com テーブルが削除されている場合は FOR SYSTEM_TIME AS OF の代わりにテーブルデコレーターを使って参照できます。 cloud.google.com Time travel Storageに配置されているデータは一定期間が経つと後述するFail-safe Storageに移動します。移動するまでの期間はTime travel windowという設定値で決まっています。Time travel windowの期間はデフォルトで7日で、最小2日〜最大7日の範囲で設定できます。 cloud.google.com Fail-safe Storage Fail-safe Storageは削除されたデータが一時的に保管される場所です。前述したTime travel Storageに保存されていたデータが次に送られる場所です。 Time travel Storageとよく似ていますが、以下の2点が異なります。 SQLやbqコマンドなどで復元できず、復元のためにサポート問い合わせが必要 保存期間を上書きできる設定はなく、7日間で固定されている Fail-safe Storageに送られてから7日間経過したデータは本当の意味で削除されます。 ライフサイクル 先ほど説明したライフサイクルの状態の変化を以下の図にまとめました。 課金モデル ライフサイクルの次に2つの課金モデルについて説明します。 cloud.google.com Logical課金 Logical課金は非圧縮状態のデータ量が計算式に使われる課金方法です。データ型毎のバイト数は以下のドキュメントに記載されており、テーブル内の全データに対してこの値を合算した値が使われます。 cloud.google.com Physical課金 Physical課金は圧縮済み状態のデータ量が計算式に使われる課金方法です。データの圧縮アルゴリズムなどの詳細は公開されていませんが、こちらの記事によるとデータによっては容量が1/20程度になることもあるそうです。 cloud.google.com なお、課金モデルはあくまで費用計算の時にのみ影響を与えるという点に注意が必要です。どちらの課金モデルを選んだとしても、実際には圧縮状態でデータが保存されています。そのため、以下のAthenaのパフォーマンスチューニングの記事にあるような圧縮・非圧縮によるパフォーマンスの差異は発生しません。 aws.amazon.com 変更方法 Logical課金を採用するかPhysical課金を採用するのかはデータセット単位で変更できます。変更するためのコマンドを実行してから実際に変更されるまでには24時間かかることと、一旦変更した後14日間は再変更ができない点に注意する必要があります。 cloud.google.com 費用 ここからストレージの分類毎の費用計算について説明していきます。ライフサイクル4種類、課金モデル2種類の組み合わせは8種類あります。 それぞれの費用を以下の表にまとめました。単位はUSD / GB / monthです。この記事ではUSマルチリージョンの費用を採用しています。他のリージョンの費用は異なりますが、傾向はUSマルチリージョンと同じです。 Logical課金 Physical課金 Active Current Storage 0.02 0.04 Long-term Storage 0.01 0.02 Time travel Storage 0 0.04 Fail-safe Storage 0 0.04 この表を使って考察をしていきます。 まずはActive Current Storageの行に着目して、Logical課金とPhysical課金について比較をしてみます。Logical課金はPhysical課金と比較して単価が半分であると分かります。しかし、Physical課金を採用すると圧縮済みのデータ量で計算されるためその影響も考慮する必要があります。圧縮の効果でデータのサイズが半分以下になるならば、Physical課金を採用したほうが安価になります。このことはLong-term Storageに対しても同様に言えます。実際に当社のBigQuery環境内にある多くのデータセットでは圧縮により半分以下のデータサイズになることが多いです。 次にTime travel Storageの行のLogical課金とPhysical課金の比較をしてみます。こちらについては先程よりも大きな違いがあります。課金モデルがLogical課金の場合はTime travel Storageの費用が無料な一方で、Physical課金の場合はActive Current Storageと同様の費用が発生します。Fail-safe Storageについても同様です。この違いを考慮せずに圧縮率だけを見てPhysical課金の方が安そうだから変更してしまうと、かえって費用を増加させてしまう可能性があります。 公式ドキュメントに載っている用語との対応 この記事で紹介した用語と公式ドキュメントの用語の対応を説明します。公式ドキュメントに書かれているActive Storageという用語の指し示すものが文脈によって異なるため注意が必要です。 料金表の用語との対応 BigQueryの料金表の用語との対応を考えます。以下の料金表には4つの用語が載っています。 Active storage Long-term storage cloud.google.com このActive Storageという用語は、本記事のActive Current Storageのみを指し示すようにも見えますが実際には異なります。Logical課金の場合はActive Current Storageのみを指します。Physical課金の場合はActive Storageに加えてTime travel StorageとFail-safe Storageのデータ量も合計したものを指します。特にPhysical課金の場合の後半2つが抜けやすいので注意が必要です。 INFORMATION_SCHEMA.TABLE_STORAGE 系のカラムとの対応 以下の記事の INFORMATION_SCHEMA.TABLE_STORAGE 系のカラムとの対応についても説明します。 cloud.google.com それぞれのカラムで取得できる情報が本記事のどれに対応しているのかを以下の表に示します。 カラム名 非圧縮か圧縮済か 定義 TOTAL_LOGICAL_BYTES 非圧縮 Active Current Storage + Long-term Storage ACTIVE_LOGICAL_BYTES 非圧縮 Active Current Storage LONG_TERM_LOGICAL_BYTES 非圧縮 Long-term Storage CURRENT_PHYSICAL_BYTES 圧縮済 Active Current Storage + Long-term Storage TOTAL_PHYSICAL_BYTES 圧縮済 Active Current Storage + Long-term Storage + Time travel Storage ACTIVE_PHYSICAL_BYTES 圧縮済 Active Current Storage + Time travel Storage LONG_TERM_PHYSICAL_BYTES 圧縮済 Long-term Storage TIME_TRAVEL_PHYSICAL_BYTES 圧縮済 Time travel Storage FAIL_SAFE_PHYSICAL_BYTES 圧縮済 Fail-safe Storage この表から注意が必要なことを読み取ってみます。 まず、Logical課金でのTime travel StorageとFail-safe Storageのデータ量を取得する方法がありません。取得できたところで結局これらの費用の単価はゼロなため取得できなくても実用上問題ないです。 次にPhysical課金にも目を向けます。Logical課金の場合の注意点は1つしかないですが、Physical課金では2つの注意点があります。まず、 TOTAL_PHYSICAL_BYTES はカラム名にTOTALという名前がついているものの、Fail-safe Storageを含んでいないことに注意が必要です。また、 ACTIVE_PHYSICAL_BYTES にはFail-safe Storageが含まれていないことも要注意です。特に先ほど紹介した料金表におけるActive Storageの定義と異なる点が罠になっています。料金表のActive StorageはFail-safeを含む一方で、 INFORMATION_SCHEMA.ACTIVE_PHYSICAL_BYTES は含んでいません。 Data retention with time travel and fail-safe内の用語との対応 以下のドキュメントに書かれているactive bytes/active storageという用語と本記事の用語の対応について説明します。ここでのactive bytes/active storageという用語は、本記事でのActive Current Storageに該当します。 cloud.google.com 文脈によるActive Storageという用語のばらつきについて ここまでで説明したようにActive Storageという用語(もしくはそれに似た用語)はドキュメントによって指し示すものが異なります。ある時にはActive Current StorageとTime Travel StorageとFail-safe Storageの3つを含みます。しかし、前者1つのみを指す場合も前者2つのみを指す場合もあります。そのため、Active Storageという単語を見かけた時には、その用語はどれを指しているのかを注意深く確認する必要があります。 テーブル毎・データセット毎の料金計算 この記事で紹介した知見を実践するために、テーブル毎・データセット毎の料金計算をしてみます。 まずは、以下のようにしてこの記事で紹介した4つのライフサイクルの状態毎、2つの課金モデル毎の容量をテーブル単位で取得します。Logical課金の場合のTime travel StorageとFail-safe Storageの容量は取得できませんが、それらは費用がゼロのため問題ないです。 select table_schema as dataset_name, table_name, deleted, active_logical_bytes / pow( 2 , 40 ) as active_current_logical_tb, long_term_logical_bytes / pow( 2 , 40 ) as long_term_logical_tb, (current_physical_bytes - long_term_physical_bytes) / pow( 2 , 40 ) as active_current_physical_tb, long_term_physical_bytes / pow( 2 , 40 ) as long_term_physical_tb, time_travel_physical_bytes / pow( 2 , 40 ) as time_travel_physical_tb, fail_safe_physical_bytes / pow( 2 , 40 ) as fail_safe_physical_tb, from `<プロジェクトID>`.`region-<リージョン>`.INFORMATION_SCHEMA.TABLE_STORAGE 次にそれぞれのテーブルの課金モデルがLogicalかPhysicalかを以下のクエリで取得します。課金モデルはテーブル毎ではなくデータセット毎に決定され、 INFORMATION_SCHEMA.SCHAMATA_OPTIONS に格納されています。この SCHEMATA_OPTIONS はSQLアンチパターンのEntity Attribute Valueのような構造をしているのでクエリを実行する時に注意が必要です。実際に、古くからあるデータセットは SCHEMATA_OPTIONS に storage_billing_model が格納されていないため、その場合の考慮を以下のクエリでは行ってます。 select dataset_name, ifnull(storage_billing_model, " LOGICAL " ) as storage_billing_model, from ( select schema_name as dataset_name, from `<プロジェクトID>`.`region-<リージョン>`.INFORMATION_SCHEMA.SCHEMATA ) left join ( -- SCHEMATA_OPTIONSにstorage_billing_modelが存在しないデータセットのためにleft joinをする select schema_name as dataset_name, option_value as storage_billing_model from `<プロジェクトID>`.`region-<リージョン>`.INFORMATION_SCHEMA.SCHEMATA_OPTIONS where option_name = ' storage_billing_model ' ) using (dataset_name) 最後に上記2つのクエリの結果をデータセットで結合させ、単価を掛け算するとテーブル毎の費用となります。実際にかかっている費用だけではなく、もしもLogical課金だったらPhysical課金だったらいくらになるのかも出力します。これによって、どちらの課金モデルを採用すると費用が安くなるのかを判断できます。 with storages as ( select dataset_name, table_name, deleted, storage_billing_model, ifnull(active_current_logical_tb, 0 ) as active_current_logical_tb, ifnull(long_term_logical_tb, 0 ) as long_term_logical_tb, ifnull(active_current_physical_tb, 0 ) as active_current_physical_tb, ifnull(long_term_physical_tb, 0 ) as long_term_physical_tb, ifnull(time_travel_physical_tb, 0 ) as time_travel_physical_tb, ifnull(fail_safe_physical_tb, 0 ) as fail_safe_physical_tb, from < 2 つめのクエリの結果> left join < 1 つめのクエリの結果> using (dataset_name) ) select *, if (storage_billing_model = ' LOGICAL ' , monthly_logical_cost_usd, monthly_physical_cost_usd) as monthly_actual_cost_usd, from ( select *, active_current_logical_tb * 20 + long_term_logical_tb * 10 as monthly_logical_cost_usd, (active_current_physical_tb + time_travel_physical_tb + fail_safe_physical_tb) * 40 + long_term_physical_tb * 20 as monthly_physical_cost_usd, from storages ) order by monthly_logical_cost_usd + monthly_physical_cost_usd desc そして、このクエリの実行結果をデータセットでGROUP BYするとデータセット毎のストレージ費用を計算できます。先程のクエリ結果をGROUP BYするだけなので、詳細なクエリはここでは省略します。 費用を節約するための戦略 最後にストレージ費用を削減するための戦略について紹介します。この章で書かれている内容を実践するためには開発工数が必要になることもあります。そのため、そもそもストレージ費用を削減することがベストなのかということを考える必要もあります。ストレージ費用ではなくクエリ実行費用を削減するほうが、コストパフォーマンス良く費用を削減できる可能性もあります。 パーティション分割 最初に紹介する戦略はパーティション分割です。ライフサイクルの状態がActive Current StorageになるのかLong-term Storageになるのかはテーブル単位ではなくパーティション単位で決定されます。そのため、データの変更が頻繁に発生する領域とそうでない領域を別のパーティションにすることによってLong-term Storageの比率を大きくできます。特にログを追記して蓄積するようなテーブルの場合はログのタイムスタンプでパーティション分割をすると、90日以上古いログの費用が半分になります。 また、このように設定したパーティション分割カラムはデータを参照するときのWHERE句に登場することも多いため、クエリ実行費用を削減する効果も見込めます。 Logical課金とPhysical課金の切り替え Logical課金かPhysical課金かはデータセット単位で設定できるため、先程のクエリでどちらのほうが安いのかを判断して切り替えると費用を削減できます。一旦切り替えた後14日間は切り替えできないため、作成された直後のデータセットに対して切り替えをすることには慎重になるべきです。ある程度の期間運用し、ストレージの傾向が安定してから切り替えを行ったほうが良いです。 圧縮率が高くなるようなデータ形式への変更 以下の資料によるとデータを取り込む前に前処理を施すと圧縮率の向上が見込めるそうです。確かに圧縮率を高めることでストレージ費用を削減できます。しかし、多くのケースでは先にクエリ実行費用を削減したほうがコスパよく費用を削減できると思います。 cloud.google.com Logical課金の方が安いテーブルとPhysical課金の方が安いテーブルのデータセットを分離 Logical課金の方が安いのかPhysical課金の方が安いのかはテーブル単位で決まる一方、どちらを採用するのかはデータセット単位でしか行えません。そのため、Logical課金のほうが安くなるテーブルとPhysical課金のほうが安くなるテーブルのデータセットを分離すると費用を削減できます。しかし、この方法は以下のような多くの副作用をもつため、基本的には採用しないほうが良いかと思っています。 データ利用者にとって分かり辛いデータセット分類になる危険性がある データセットの移動をする時、Long-term Storageに配置されていたデータがActive Current Storageに移動する。そのため、かえって費用が増加するかもしれない まとめ この記事ではBigQueryのテーブル費用をライフサイクルと課金モデルという2軸から体系的に説明しました。特にActive Current Storageという公式ドキュメントに載っていない概念を意識するとBigQueryの料金計算に関する解像度が上がります。公式ドキュメントに載っているActive Storageという曖昧な表現を見たときにはその用語の意味を明確にして読み進める必要もあります。 ZOZOではデータアナリスト、データマネージャー等のさまざまなポジションで一緒に働く仲間を募集中です。カジュアル面談も実施しておりますので、ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com www.wantedly.com *1 : 千葉県の水溶性天然ガス概要
はじめに こんにちは。データシステム部推薦基盤ブロックの新卒1年目の上國料( @Kamiko20174481 )と、5年目の宮本( @tm73rst )です。私たちのチームでは、ZOZOTOWNの推薦システムを開発しています。2024年7月のテックブログでは、ZOZOTOWNのホーム画面に表示される「 モジュール 」の並び順をパーソナライズする取り組みを紹介しました。 techblog.zozo.com モジュール とは、トレンドやキャンペーンなど特定のテーマに基づき商品群を表示する枠のことです。 モジュールの内容は企画チームの意図に基づいて設定されますが、ユーザーごとに関心や求めるコンセプトが異なるため、一律の表示ではなく最適な順序で並べることが重要です。 このように、ユーザーごとに適したモジュールを配置する仕組みを モジュールパーソナライズ と呼びます。本記事では、このモジュールパーソナライズの精度を向上させるために実施した「モジュールの多様性向上」と「受注系指標の改善」についてご紹介します。 パーソナライズ機能や推薦システムの開発に携わる方々の参考になれば幸いです。 目次 はじめに 目次 モジュールパーソナライズ Two-Towerモデルによる推薦 モジュールパーソナライズの仕組み モジュールパーソナライズをリリースしてわかった課題 1. 多様性の欠如 パーソナライズロジックの過度なクリック履歴依存 モジュール間での商品の重複表示 2. 購入促進の難しさ 改善のアプローチ パーソナライズの方針 Two-Towerモデルの精度向上 後処理の改善・導入 A/B テスト 概要 リリースを行う際の実験群の選定方針 結果 受注系指標 多様性に関する指標 Treatment1(クリック最適化モデル + 後処理)について Treatment2(カート投入最適化モデル)について Treatment3(カート投入最適化モデル + 後処理)について 今後の展望 パーソナライズのリアルタイム化 商品の並び順のパーソナライズ 推薦指標の定式化 最後に モジュールパーソナライズ ZOZOTOWNではユーザーの興味を引く商品を効果的に訴求するため、ホーム画面に多様なモジュールを表示しています。しかし、すべてのユーザーに同じモジュールを一律に表示すると、ユーザーによっては興味の薄いコンテンツが前面に出てしまい購買促進につながりにくいという課題がありました。 そこで、 ユーザーの嗜好に応じてモジュールの並び順を最適化する「モジュールパーソナライズ」 を導入しました。本手法の中核となるのが Two-Towerモデル です。 Two-Towerモデルによる推薦 モジュールパーソナライズは、以下の図に示す Two-Towerモデル を利用して構成されています。 Two-Towerモデルでは、 ユーザーの特徴 と 商品の特徴 をそれぞれ個別のニューラルネットワーク(タワー)で学習し、共通の埋め込み空間へマッピングします。類似した特徴を持つユーザーと商品が近い位置に配置されるよう学習されるのが特徴です。 具体的には、 ユーザーの属性 (例:年齢、性別、閲覧履歴など)を元にユーザー埋め込み(ユーザーの嗜好を表すベクトル)を計算し、 商品の属性 (例:カテゴリ、ブランド、価格帯など)を元に商品埋め込み(商品の特徴を表すベクトル)を計算します。 推薦する際は、ユーザー埋め込みと候補商品の埋め込みのコサイン類似度を算出し、ユーザーの嗜好に合った商品をランキング化します。 モジュールパーソナライズの仕組み ZOZOTOWNのホーム画面における推薦対象は 商品単体ではなく、複数の商品を含むモジュール です。そこで、以下の手順で モジュール単位のランキング を行います。 各商品の推薦スコアを算出(Two-Towerモデルによるスコアリング) モジュール内の商品のスコアを集約 統合スコアが高い順にモジュールを並び替え このフローにより、ユーザーの興味に合ったモジュールが上位に表示され、より最適なコンテンツを提供できるようになります。 モジュールパーソナライズをリリースしてわかった課題 モジュールパーソナライズの導入により判明した課題は、 多様性の欠如 と 購入促進の難しさ の2点です。 1. 多様性の欠如 多様性はやや抽象的な概念ですが、ここでは以下の2つの観点で課題を整理します。 パーソナライズロジックの過度なクリック履歴依存 ユーザーの商品クリック履歴は、特定のカテゴリや同一商品に偏る傾向があります。これは購入検討時に類似商品を比較したり、関心のあるカテゴリを集中的に探索するといった行動が要因です。しかし、従来のモデルは 直近のクリックデータに最適化 されているため、その履歴に強く依存してしまうリスクがあります。その結果、最近クリックしたカテゴリの商品ばかりが上位に表示され、ユーザーの探索の幅が狭まります。また、一時的に興味を持った商品や意図せずクリックした商品が学習へ影響を与え、ユーザーの本来の嗜好を正確に捉えにくくなる可能性もあります。 モジュール間での商品の重複表示 従来のZOZOTOWNのホーム画面では、同じ商品が複数のモジュールにわたって重複表示されるケースがありました。 特に、モジュールごとに横スクロールなしで表示される商品群は、サイト全体の印象やユーザーの探索行動に大きく影響を与えるため、重要な要素です。本稿では、これらの商品群を「ファーストビュー」と定義します。 なお、一般的に「ファーストビュー」は 縦スクロールなしで画面上部に表示される範囲 を指すことが多いですが、本稿では 各モジュール内で横スクロールなしに表示される商品群 を意味する点にご注意ください。 ファーストビューで同一商品が繰り返し表示されると、以下の問題が発生します。 商品の偏り :特定の商品やカテゴリへの露出が過剰になり、多様な商品が目に留まりにくくなる 新しい発見機会の減少 :同一商品の露出が増えることで、ユーザーが新しい商品に出会う機会が制限される 探索意欲の低下 :同じ商品が目立つことで新鮮さが薄れ、ユーザーの縦スクロールや横方向への探索意欲が低下しやすくなる このため、ファーストビューでの商品重複は、ユーザー体験の質を低下させる要因となっています。 2. 購入促進の難しさ 従来のロジックを用いたA/Bテストの結果、受注に関する指標のさらなる向上の可能性が明らかになりました。特に、パーソナライズロジックがクリック履歴に強く依存していることで、ユーザーの本来の嗜好を正確に捉えきれていない可能性があります。この影響により、推薦されたモジュール内の商品がユーザーの購入意欲を十分に喚起できず、購買へつながりにくい状況になっていると考えます。 改善のアプローチ パーソナライズの方針 課題を解決するために、図に示すように以下の対応をします。 Two-Towerモデルの精度向上 後処理の改善・導入 これにより、ユーザーの嗜好に即したモジュールのスコアリングを行い、後処理によってより多様な商品がユーザーに届くよう順序を最適化し、上述の課題を解決します。ここでいう「ユーザーの嗜好に即した」とは、 購入へつながる確度が高い ものを指します。 Two-Towerモデルの精度向上 今回モデルを改善する目的は、「ユーザーの短中期的な嗜好を学習し、それに即した多様な推薦をし最終的に購入に促す」ことです。主な改善ポイントは、以下の2点です。 最適化指標の変更 UserTowerの特徴量の変更 まず、 最適化指標 については、従来の「クリック」から「カート投入」に切り替えました。クリックは単なる興味関心の指標に過ぎないのに対し、「カート投入」はユーザーが実際に購入を検討した行動であり、より購買意向に近い嗜好を反映していると考えたためです。一方で、「購入」を最適化指標として採用しなかったのは、データの絶対量が少なく、学習が不安定になる懸念があったためです。そのため、 クリックよりも購買意向に近く、かつ十分なデータが確保できる「カート投入」 を最適化指標として選定しました。 次に、 UserTowerの特徴量 については、これまで「直近のクリック履歴(時系列データ)」のみを利用していましたが、 過去1か月のクリックログを活用する方針に変更 しました。中期的なログを学習することで、ユーザーの継続的な嗜好を捉え、より多様な商品を推薦しやすくなると考えています。具体的には、「商品」「ブランド」「カテゴリ」「カラー」などの複数の軸でユーザーの関心が高い要素を集計し、上位のものを抽出します。さらに、これらの情報に時系列の重み付けをし、直近の行動と過去の一貫性をバランスよく反映できるよう工夫しています。 細かな変更点としては、データ分割におけるトランザクション期間を1週間に設定し、訓練・検証・テストデータそれぞれで四分位範囲を用いたフィルタリングを行いました。これにより、学習から評価までの過程でデータ分布が大きく偏る(Skew)リスクを最小限に抑えつつ、分割の一貫性を高めることができます。 以下では、従来モデルを クリック最適化モデル 、改善後のモデルを カート投入最適化モデル と呼ぶことにします。 後処理の改善・導入 後処理を導入する目的は、「Two-Towerモデルでスコアリングされたモジュール群から、重複や偏りを抑えつつ多様な商品をユーザーに届け、体験価値を向上させる」ことです。主な改善ポイントは、以下の3点です。 モジュールのスコア計算手法を変更 類似モジュールの連続表示を抑制 関心度に応じたモジュールの再配置 まず、 モジュールのスコア計算手法の変更 についてです。従来は、モジュール全体の商品を対象にスコアを計算していました。今回はホーム画面の縦スクロールというUI特性に合わせ、 ファーストビューのみを対象にスコアを計算するよう変更 しました。これにより、ユーザーが実際に注目する領域に基づいた評価が可能となり、より適切なモジュールの順位付けが期待できます。 次に、 類似モジュールの連続表示の抑制 についてです。従来、同一商品を含むモジュールが連続して上位に表示されることがありました。これは、異なるモジュール間でスコアを独立に算出するため、高スコアの同一商品を含むモジュールが複数上位に配置されやすかったことが原因です。そこで、 各モジュールのファーストビューに同じ商品が多く含まれている場合、スコアが低いモジュールの順位を下げる仕組みを導入 しました。これにより、ホーム画面上での商品の重複表示を抑え、多様性の向上を図ります。 最後に、 関心度に応じたモジュールの再配置 についてです。埋め込みを用いたスコアリングには予測誤差が生じるため、ユーザーの本質的な嗜好を完全には捉えきれない可能性があります。そこで、 直近でユーザーが閲覧したもののクリックしなかったモジュールは迅速に下位へ移動させ、新しいモジュールを上位に推薦する仕組みを導入 しました。これにより、ユーザーの関心に即したモジュールを素早く提示し、飽きを防ぎつつ最適なモジュールを推薦できます。 A/B テスト 概要 新ロジックの効果を評価するために、ZOZOTOWN会員を対象として 5週間のA/Bテスト を実施しました。現行ロジックと前述のアプローチを組み合わせた 以下の4つのパターン を用意しました。また、テスト対象のユーザーが各実験群に均等に振り分けられるよう、4つのグループそれぞれに25%ずつ割り当てています。 実験群 説明 Control クリック最適化モデル Treatment1 クリック最適化モデル + 後処理 Treatment2 カート投入最適化モデル Treatment3 カート投入最適化モデル + 後処理 リリースを行う際の実験群の選定方針 A/Bテスト開始前に、プロジェクトメンバー間でリリース判断の基準を明確化しました。具体的には、受注系指標だけでなく、多様性に関する指標も考慮することを原則としました。ただし、多様性が向上してもKGIやKPIが悪化する場合はリリースを見送る方針とし、全メンバーで認識を統一しました。 結果 A/Bテスト結果のサマリを受注系の指標と多様性に関する指標に分けて以下に示します。 受注系指標 指標 備考 T1/C 比(%)  T2/C 比(%)  T3/C 比(%)  ホーム画面訪問者の受注金額 ホーム画面にランディングしたユーザーの合計受注金額 99.9 100.2 100.1 モジュール経由の受注金額 モジュールに表示された商品の合計受注金額 100.2 100.9 101.2 モジュール経由の受注商品点数 モジュールに表示された商品の合計受注商品点数 100.4 101.2 100.9 モジュール経由のカート投入数 モジュールに表示された商品の合計カート投入数 100.2 101.4 101.3 多様性に関する指標 指標 備考 T1/C 比(%)  T2/C 比(%)  T3/C 比(%)  モジュール掲載商品のユニーク閲覧数 表示商品の合計ユニーク閲覧数 101.8 99.8 101.4 モジュール掲載商品のユニーククリック数 表示商品の合計ユニーククリック数 100.5 100.0 100.4 モジュール間の閲覧商品の重複率 モジュール間で同じ商品を閲覧した割合 92.0 98.4 91.5 モジュールの重複率 前日と比較して同じモジュールが表示された割合 98.3 106.3 106.1 商品の多様性 ユーザーごとのユニーク閲覧商品数 / 全体の閲覧商品数 100.4 100.0 100.3 カテゴリの多様性 ユーザーごとのユニーク閲覧カテゴリ数 / 全体の閲覧商品数 100.3 101.3 101.6 ブランドの多様性 ユーザーごとのユニーク閲覧ブランド数 / 全体の閲覧商品数 99.8 99.0 99.2 Treatment1(クリック最適化モデル + 後処理)について 従来のロジックでは、特定の商品や子カテゴリに偏りが生じ、新しい商品の発見機会が制限されていました。しかし、商品の多様性を考慮した後処理を追加した結果、ユーザーが閲覧する商品のバリエーションが広がり、 多様性に関するほぼすべての指標が飛躍的に向上 しました。さらに、クリック数や閲覧ページ数の増加も確認され、ユーザーが新しい商品を発見しやすくなったことが示されました。このことから、後処理の導入が興味喚起の向上に寄与したことが実証されたと考えられます。 一方で、 受注系指標への大きな影響は見られませんでした 。その要因の1つとして、モジュールのスコアリングに使用した既存のクリック最適化モデルが、直近のクリックログに引っ張られ偏ったカテゴリや商品を推薦していた点が挙げられます。そのため、後処理を追加しても一部のケースではユーザーの嗜好を十分に反映できていなかった可能性があります。 Treatment2(カート投入最適化モデル)について カート投入最適化モデルは、「短中期的な嗜好を学習し、多様な推薦をすることで購入を促進する」という設計意図を実現し、 受注系指標が大幅に向上 しました。短中期的な嗜好の学習により、カテゴリや商品の偏りを抑えながら一貫性のある推薦をし、ユーザーに幅広い選択肢を提供した結果、売上向上に寄与したと考えられます。 一方で、「ブランドの多様性」は減少しましたが、定性評価ではユーザーの関心が高いブランドの露出が増加し、過去のクリック履歴に含まれていないブランドも、嗜好に基づく推薦が適切に機能していることが確認されました。 また、「モジュールの重複率」は増加しましたが、A/Bテスト結果を基に追加分析を行ったところ、重複したモジュールは売上と正の相関を持つことが判明しました。この結果から、モジュールの重複は必ずしもネガティブな影響を与えるとは言えず、むしろユーザーに適切な商品を継続的に提示することが有効な手段となり得ると考えられます。 しかし、 商品の多様性やユニーク商品に対する閲覧数・クリック数の向上は確認されませんでした 。この要因として、短中期的な嗜好を学習することで、ユーザーの過去の行動に基づいた推薦が強化され、一貫性のある提案が可能になったことが挙げられます。これにより、関連性の高い商品が表示されやすくなる一方で、ログの即時性が低下し、推薦内容の変化が抑制される傾向があると考えられます。 Treatment3(カート投入最適化モデル + 後処理)について カート投入最適化モデルと後処理を個別に適用した場合、それぞれ特定の指標には良い影響を与えたものの、一部の指標にはネガティブな影響も見られました。しかし、両者を統合したモデルでは互いの弱点を補完し合い、全体的なスコアの向上が確認され、各指標のバランスを取ることができました。 結論として、 カート投入最適化モデルによりユーザーの嗜好に即したスコアリングをするとともに、後処理でより多様な商品がユーザーに届くよう順序を最適化することで、受注系指標と多様性に関する指標の向上を両立させることができました 。 この結果を踏まえ、受注系指標および多様性に関する指標を総合的に評価した上で、Treatment3のリリースが決定しました。 今後の展望 モジュールパーソナライズのTwo-Towerモデルをカート追加最適化モデルに切り替え、後処理を追加することで、 多様性と受注系の指標の向上 を実現しました。今後は以下のような取り組みを行う予定です。 パーソナライズのリアルタイム化 商品の並び順のパーソナライズ 推薦指標の定式化 パーソナライズのリアルタイム化 カート投入最適化モデルへの切り替えにより、モジュール内の商品の重複率が増加し、ユニークな閲覧数やクリック数が減少しました。これは、短中期的な嗜好を学習した結果として生じたものであり、意図した効果ではあるものの、さらなる改善の余地があります。 現行のシステムでは、推論パイプラインが1時間に1回実行され、ユーザーのパーソナライズ情報が更新されます。従来のロジックでは、更新頻度を上げると直近の閲覧履歴に過度に影響され、ユーザーの短期的な嗜好が強く反映されるという課題がありました。しかし、カート投入最適化モデルでは、中期的な嗜好も考慮することで、短期的な変動に左右されにくい推薦が可能になっています。 この特性を活かし、リアルタイムでのモジュール更新を導入すれば、短期嗜好への過剰適応を抑えつつ、ユーザーの関心に応じた商品を最適なタイミングで推薦できると考えています。 商品の並び順のパーソナライズ 現在、一部のモジュールではユーザーごとに商品がパーソナライズされているものの、多くのモジュールでは共通の商品が表示されています。また、モジュールパーソナライズだけでは、ユーザーごとの微妙な嗜好の違いを十分に捉えきれない場合があります。例えば、同じカテゴリの商品を表示するモジュールであっても、ユーザーごとに好むブランド、価格帯、デザインの傾向は異なります。 現行のシステムでは、表示する商品の選定は行われているものの、ユーザーごとの関心度に応じた並び順の最適化は行われていません。そのため、より個々の嗜好に適した推薦を実現するには、商品の表示順をユーザーごとに最適化することが重要です。現在、このパーソナライズ機能の開発を進めています。 推薦指標の定式化 今回の取り組みでは多様性を重視しましたが、今後は新規性やセレンディピティなど、さらなる推薦指標の改善にも取り組む予定です。推薦指標は、企業やサービスごとに定義が異なるため、適切な指標の選定が重要になります。 そこで、私たちが目指すZOZOTOWNのホーム画面に最適な推薦指標を定義し、それをどのように改善につなげるかをチーム内で議論しながら具体化していきます。 最後に 本記事ではZOZOTOWNのホーム画面に表示するモジュールの並び順をパーソナライズするシステムとその効果について紹介しました。今回取り上げた部分以外にも改善すべき箇所が大量にあるので、これからも1つずつ改善していくことでユーザーにとってより良いZOZOTOWNを提供できるよう邁進していきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに データシステム部検索技術ブロックの内田です。私たちはZOZOTOWNの検索精度改善や検索システムの運用効率化のためのメンテナンスなどに取り組んでいます。 これまでテックブログでご紹介してきた通り、ZOZOの検索改善チームではランキング学習(Learning to Rank)やクエリの意図解釈、ベクトル検索の導入など、比較的モダンなアプローチでZOZOTOWNの検索改善に努めてきました。先進的な技術を調査し、サービスの開発に応用することはサービスの品質改善において重要な取り組みです。 techblog.zozo.com しかし、モダンなアプローチをとる一方で、検索エンジンのベーシックな設定についてはメンテナンスする機会が徐々に減少していきました。設定内容や経緯を把握している開発メンバーの割合も減っていき、このままだと誰も触れない謎の設定になってしまうリスクがあったため、一度見直しを実施することにしました。 この記事では、全文検索エンジンの基礎的な設定について見直しを始める際に意識した内容を紹介します。これから検索システムを構築する方、全文検索エンジンの設定について学ぶ方の参考になれば幸いです。 全文検索システムの設定の基礎 全文検索とは、与えられたクエリにマッチするテキスト情報を持つ文書を発見する技術です。Luceneを代表とする全文検索エンジンは、文書に含まれるテキスト情報を解析して索引(インデックス)を構築することでクエリにマッチする文書の発見の高速化を実現しています。 ここで重要となるのが、 文書中のテキスト情報からインデックスに登録されるトークンをどのように抽出するか という問題です。トークン抽出の品質は、全文検索全体の精度に大きく影響を与えます。そのため、トークン抽出を担うアナライザー(解析器)の設定は非常に重要です。 Apache SolrやElasticsearchなどのLuceneベースの全文検索エンジンでは、アナライザーは以下のような三層構成をとります。入力が想定されるクエリや文書に含まれるテキスト情報に応じてそれぞれを設定します。クエリに対して適用するアナライザーと文書中のテキスト情報に適用するアナライザーは別々に設定できますが、出力されるトークンの一致が取れるよう設計する必要があります。 Character filter:入力されたテキストに対する加工処理 Tokenizer:トークンへの分割処理 Token filter:トークンに対する加工処理 テキスト情報に応じたアナライザー設定 N-Gramと形態素解析 先述した通り、検索エンジンに文書を登録する際にはテキスト情報をトークンに分割する必要があります。英語など西洋の多くの言語では空白文字で単語が区切られますが、日本語の文章は明確な区切り文字を持ちません。そのため、日本語で構成される文書に対しては、N-Gramと形態素解析に基づいたトークン分割が採用されることが多いです。ZOZOTOWNの検索システムでも検索対象フィールドに対しては、この2種のトークン化手法に基づいたアナライザーが設定されています。 N-Gramは文章をN文字ごとに区切ります。一方で形態素解析は辞書に基づき意味のある単位で文字列を区切ります。 利点 欠点 N-Gram ・検索漏れが少ない(再現率が高い) ・辞書が不要 ・検索ノイズが多い ・無差別に分割するため、インデックスのサイズが肥大化しがち ・N文字以下の文字列入力に対してトークンを出力できない 形態素解析 ・検索ノイズが少ない(適合率が高い) ・インデックスのサイズが小さめ ・品詞に基づくフィルタリングや変形が可能 ・辞書が必要 ・辞書に過不足があり単語を正しい位置で区切れなかった場合に検索精度が低下する (主に未知語による検索漏れが起こりやすい) この通り、N-Gramと形態素解析はそれぞれ得意とする領域が異なります。どちらを適用するかは取り扱うテキスト情報の特徴に合わせて検討する必要があります。例えば、ZOZOTOWNで取り扱う商品のテキスト情報には以下のような特徴があります。 商品名:主に名詞で構成されるため、品詞に基づく処理を必要としない。ファッション系の商品名はカタカナの連語で構成される固有名詞(未知語)であることが多く、形態素解析器では正しい位置で区切ることが難しい。 商品説明文:入力テキストが大きく、比較的多量のトークンが抽出される。日本語の文章で記述されていて、検索上意味を持ちにくい助詞などの形態素を数多く含むため品詞によるフィルタリングを行いたい。 これら2つの手法を適用したフィールドを横断的に検索するクエリを用いることで検索の精度を向上させることが出来ます。代償として検索処理の計算コストやインデックスのサイズおよび構築時間が膨らむため、それに見合った精度向上が期待できるかは検証するとよいでしょう。また、それぞれのフィールドに対するスコア重み付けの調整も重要です。 辞書 現在、ZOZOTOWNの検索システムに設定されている各種辞書の見直しに取り組んでいます。見直しを進める中で、逆に検索精度面に問題を発生させてしまっている設定が見受けられました。その一例を紹介します。 形態素解析器のユーザ辞書 ZOZOTOWNの検索システムは形態素解析器としてKuromojiを採用しています。Sudachiなど他の形態素解析器では挙動が異なる可能性があるためご注意ください。 形態素解析器の辞書は形態素解析処理の根幹となるため、検索の精度に大きく影響を及ぼします。文書中に出現する語句を多くカバーするためにユーザ辞書に単語を片っ端から追加してしまいがちです。ZOZOTOWNの検索システムの形態素解析ユーザ辞書にも過去に様々な語句が登録されていました。 ここで注意が必要なのが、ユーザ辞書に登録された語は優先的に区切られやすくなるというKuromoji tokenizerの挙動です。例えば、ZOZOTOWNではアクセサリーの略語である「アクセ」がユーザ辞書に登録されていました。これにより、「アクセ」を部分文字列に含む文章は概ね「アクセ」トークンを含むように分割されるようになります。結果として「アクセサリー」を意図して「アクセ」というクエリで検索したユーザに対して、全く関連のない商品を返してしまうことがありました。 GET _analyze { " tokenizer ": { " user_dictionary_rules ": [ " アクセ,アクセ,アクセ,カスタム名詞 " ] , " type ": " kuromoji_tokenizer " } , " text ": [ " アクセント " ] } // Output: { " tokens ": [ { " token ": " アクセ ", " start_offset ": 0 , " end_offset ": 3 , " type ": " word ", " position ": 0 } , { " token ": " ント ", " start_offset ": 3 , " end_offset ": 5 , " type ": " word ", " position ": 1 } ] } このような挙動は辞書に登録された短い語句で頻繁に発生していました。そのため、文書に登場する未知語を片っ端からユーザ辞書に登録するのではなく、以下のケースに絞って登録する方針で整理を進めています。 語句が意図せず区切られてしまっている場合、区切らせないためにその語句を登録する 途中で区切られてほしい語句(フレーズ)が区切られない場合、その語句の区切り方を登録する シノニムの辞書 検索の再現率を向上させるためにSynonym (graph) token filterを利用することがあります。辞書に語句の同義関係を記述することで、検索時にトークンの拡張を行えます。 Synonym token filterは、元のトークンとシノニム拡張で得られたトークンを同等の重みで扱う点に注意が必要です。スコア計算式によっては、拡張後のトークンにマッチした文書が元のトークンにマッチした文書よりも高いスコアを持つことがあります。ヒット件数を増やしたいという動機で軽率に同義関係を増やしてしまうと、検索の精度に悪影響を及ぼす危険性があります。 例えばZOZOTOWNでは、シノニム辞書に「リング」と「指輪」が同義語として登録されていました。「指輪」で検索した際に、ピンキーリングなどの指輪商品をヒットさせたかったものと思われます。これら2つの語は似た意味を持ちますが、厳密には同義ではなく「リング」は指輪に限らず輪形のもの全般を指す語です。結果として、指輪を探すユーザに対して、イヤリングやスマホリングなどを返してしまう不具合を発生させてしまっていました。 シノニム拡張は再現率を向上させるのに有用ですが、スコアの制御ができないため、同義ではなく類義の語まで適用範囲を拡張してしまうと思わぬ結果を招くリスクがあります。類義語でクエリを拡張したい場合は、アナライザー内で処理するのではなくクエリの構築時に重み付きのOR条件で記述する方が制御が容易になると思われます。ZOZOTOWNでもシノニム辞書で扱っている語句を整理して、一部はクエリ構築時に展開処理を施す方針を検討しています。 まとめ 本記事では、Elasticsearchのアナライザー設定について解説しました。また、ユーザ辞書の登録方針やシノニム拡張のリスクについて具体例を交えて説明しました。 ZOZOでは今後も引き続き、有益な検索結果を提供できるよう検索機能の改善に努めていきます。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、計測システム部フロントエンドブロックの平田です。 私が所属する計測フロントエンドブロックでは ZOZOMETRY というスマートフォンを用いて身体計測し、計測結果を3Dモデルやデータとして可視化し、Web上で管理できるtoBサービスを開発しています。 このサービスのフロントエンドではReact(Next.js)を採用しています。更にそれらの知見を深めるために、NYで開催されたJSNation、React Summit US 2024、そしてWorkshopに参加してきました。 この記事では現地参加ならではの経験や、参加したセッションへの考察、Workshopで学んだ内容などを紹介していきます! はじめに JSNationとReact Summitとは? Day 1 - JSNation Day 2- React Summit After Party 気になったセッションについて Chrome DevTools 2024: Debugging and Performance Optimization for React Developers AIアシスタントによるデバッグの向上 React Developer Toolsによる拡張性と最適化 ローカルオーバーライド機能での実験と検証 パフォーマンス最適化ツールの活用 3Dレイヤーとアニメーションデバッグ まとめ Green Bytes: How Enhancing Web Vitals Contributes to Environmental Sustainability まとめ Day 3 - Workshop ワークショップ内容 Server Components Client Components Client Router Server Actions イベント全体を通じて感じたこと 最後に JSNationとReact Summitとは? JSNation と React Summit は、JavaScriptおよびReactに特化した国際的なカンファレンスで、 GitNation が主催しています。現地参加とオンライン参加のハイブリッド形式で開催され、それぞれJavaScript、React.js関連の様々なセッションが行われます。また、ネットワーキングやワークショップ、アフターパーティーなど、多彩なプログラムが提供されています。イベントでは最新の技術の動向を学び、世界中の開発者と交流する絶好の機会を体験できます。 様々な国や地域で開催されていますが、今回は2日間を通じてアメリカで開催されたJSNation、React Summitに参加してきました。ZOZOからは昨年オランダ・アムステルダムで開催された同イベントにもエンジニアが参加しており、今回も引き続き参加できることになりました。そして3日目は、1日通しで行われるワークショップに参加してきました。 日付 時間帯(EST) イベント 場所 2024/11/18 9:00 - 17:00 JSNation Liberty Science Center 2024/11/19 9:00 - 18:00 React Summit Liberty Science Center 2024/11/19 19:00 - 22:00 After Party Barcade & Hudson Hound 2024/11/20 9:00 - 18:00 Workshop Double Tree by Hilton それでは早速参加したそれぞれのイベントについて詳しく紹介していきたいと思います。 Day 1 - JSNation 公式サイト には「In New York」や「Manhattan views」と大きく書かれています。しかし実際の会場はNew York Cityの隣、Jersey CityにあるLiberty Science Centerです。間違えないようにしましょう。 会場に向かう途中で見た朝のマンハッタンの風景 最寄駅から会場に向かう途中の道 丁度紅葉シーズンだったので木々が秋色に色づいてました 会場のLiberty Science Center外観 会場に到着してチェックインすると、ステッカーやJSNationオリジナルロゴ入りのマグカップをもらいました。 階段もJS仕様になっています。 開場してすぐの8時過ぎに到着したのでまだ人がまばらでした。 Visitorバッジ。ラストネームのスペルを間違えて登録していました。 このQRコードには個人情報が紐づけられていて、イベント中のネットワーキングに大活躍します。このバッジは2日間を通して使用するため紛失しないようにしましょう。ちなみに私は2日目に新しいバッジが配布されると思い込みホテルに忘れてしまいました。 砂糖たっぷりの喉が焼けるほど甘いドーナツ。会場にはドーナツやペイストリー、フルーツなどが用意されており、朝ごはんを心配する必要はありませんでした。 参加者の中には会社のチーム単位で参加している人もいれば1人で参加している人もいました。私は近くにいたサウスカロライナ、ニューヨーク、ドイツから来た参加者と仲良くなり、そのまま一緒に行動しました。 軽く自己紹介をして、どんな仕事をしているか、どんな技術を使っているか、そしてどんなモチベーションでこのイベントに参加しているのかを話しました。自己紹介の際に自分が携わっているZOZOMETRYについて話したら、皆が口々に「クール」と言ってくれてちょっと嬉しかったです。 また、会場で出会ったほとんどの人が使っている技術はReactでしたが、ドイツ人エンジニアは出会った人の中で唯一Angularを使っていました。その人の会社は現在Angularを使って開発しているだけでなく、ドイツ国内でAngularの使い方を教えることもしているそうです。 そんなこんなで盛り上がっているうちにオープニングの時間となり会場に移動しました。 オープニングの様子。(写真提供:GitNation) 司会グループがラップを披露していました。(写真提供:GitNation) 最初のセッションの様子です。(写真提供:GitNation) 会場では、メイン会場ともう1つの会場で同時に2つのセッションが行われ、参加者は興味のあるセッションを選んで参加する形式でした。私は事前に参加するセッションをある程度決めておきました。 セッションの後には毎回質問ブースで個別質問できるSpeakers Q&A Roomもありました。(写真提供:GitNation) ランチメニューには多様性への配慮を感じさせるラインナップが揃っていました。(写真提供:GitNation) いくつかセッションに参加したり色々な人と話したりしていると、あっという間にランチタイムです。 ランチタイムには仲良くなった人々と合流して、のんびりと食事を楽しみました。直前のセッションがWebのパフォーマンスに関する内容だったため、どのようなツールを使っているかについて話が盛り上がりました。今のチームでDatadog RUMを使っていることを話すと「Synthetic Monitoring Tool」を勧められました。外部のエンジニアと気軽に相談できる環境はとても素敵ですよね。 技術的な話だけでなく、NYC観光情報についても盛り上がりました(残念ながら観光をする機会がなかったので、情報を活かす場はありませんでしたが)。また、最近の厳しい北米のジョブマーケットの話や、各国の休暇制度の違いについても話題になりました。特に、日本の企業では病気休暇が一般的には存在しないことに驚愕されました。様々な話題で盛り上がりとても楽しいひとときでした。前職まで西海岸の某所で働いていたこともあり、どこか懐かしい雰囲気を感じました。 またセッション以外には、スピーカーとカンファレンス参加者が意見を交わすディスカッションコーナーが設けられており、リスナーとしても参加可能でした。私はその時間を逃してしまったものの、果敢にディスカッションに参加した人から「周りのレベルが高すぎて、自信が無くなりそうになった」という感想を聞きました。 ディスカッションルームの風景(写真提供:GitNation) その他は企業ブースもたくさん来ておりノベルティハントしつつお話を聞いて回ったりしました。 「Storybook」ならぬ「Storyblok」という会社があり、CMSツールを提供している企業です。(写真提供:GitNation) セッション会場外の様子(写真提供:GitNation) 砂糖たっぷりのおやつ Day 2- React Summit 2日目はReact Summitです。この日もチェックイン後にマグカップとステッカーが貰えました。 React Summitの会場内観(写真提供:GitNation) 会場に入ると前日のJSNationと比べて参加者数や企業ブースも増え、より賑やかな印象を受けました。また、前日カジュアルな格好をしていた参加者がスーツ姿に変わっているなど、皆の気合いも一段と高まっているように感じました。 オープニング開始前に以前チームで導入を検討していたSentryのブースを訪れました。当時Sentryに対してほとんど知識がなく手探り状態だったので、導入コストやNext.jsとの相性など基本的なことから、使用方法、現在使っているDatadog RUMとの違いなどを質問しました。話を聞いた印象としてはDatadog RUMと似たような印象を受けたのでもう少し深く聞こうとしたところでタイムオーバーとなり少し残念でした。 企業ブース(写真提供:GitNation) React Summitのオープニング(写真提供:GitNation) React Futureのパネルディスカッション風景(写真提供:GitNation) 前日のJSNationではもちろんJavaScriptに関するセッションもありましたが、チーム内コミュニケーションに関するものなど、一般的な内容も多かった印象です。一方React Summitのこの日は、ほとんどのセッションがより技術的な内容にフォーカスしていたように感じました。 ランチメニュー。手前から3番目の豆腐を焼いた食べ物が美味しかったです デザート。イタリアンスイーツのカンノーリもあったのですが出遅れたせいで全て完売していました。(写真提供:GitNation) この日のランチタイムは、前日とは違うメンバーと一緒に過ごしました。セッションの感想や、どんな仕事をしているのか話しているうちに、気づけばピザ談義で大盛り上がりでした。 皆様によると、どうやらNYのピザやベーグルが世界一と評される理由は「水が綺麗だから」らしいのです。これを読んでいる皆さん同様私も最初は疑いましたが、後にChatGPT先生にも確認したところ、同じ説明をされてしまいました。ちなみにシカゴ出身の方曰く「シカゴピザこそが世界最高」だそうです。 こうしてピザ論争が続いたわけですが、実のところ私はピザが好きではないのでした。 企業ブースの中には高額商品が当たるイベントなども用意されていました。(残念ながら当選しなかったです。) イベントエンディングの様子(写真提供:GitNation) 気づけばあっという間に2日目も終了しました。この2日間はエネルギッシュな雰囲気に触れ、非常に刺激的で充実した時間を過ごすことができました。 After Party React Summitの後には、After Partyの場が用意されていました。2つのバーがイベント参加者専用に確保されており、1つは静かに会話を楽しみたい人向けの Hudson Hound 、もう1つは賑やかに過ごしたい人向けの Barcade でした。 私はゆっくりと会話を楽しみたかったのでHudson Houndへ行きました。イベント中に仲良くなった人や、その時初めて出会った人々とこの2日間のイベントはどうだったか、どのセッションが一番良かったかなど話しました。そこで聞いて驚いたことがあったのですが、中にはGitNationに招待されて参加費無料で来ている人もいるそうです。招待された人は、別のカンファレンスで登壇したことをLinkedInに投稿したところ、GitNationの目に留まり招待が来たそうです。興味がある方は試してみてもいいかもしれないですね。 Hudson Houndの内観。落ち着いた環境でゆったりと過ごせたので、非常にリラックスできました。 仲良くなった人々と。 Barcadeの内観 バー周辺の様子。綺麗で落ち着いていて路上バイオリニストなどもいて夜でも平和でした。 気になったセッションについて それでは、特に印象に残ったセッションを2つご紹介します。これらはGitNationのウェブサイトでも視聴可能ですので、ぜひチェックしてみてください。 gitnation.com Chrome DevTools 2024: Debugging and Performance Optimization for React Developers gitnation.com まず1点目はGoogle ChromeのエンジニアリングリーダーのAddy OsmaniさんのAIを活用したデバッグ支援やパフォーマンス最適化機能のセッションでした。 プレゼンテーションの様子(写真提供:GitNation) 以下、気になったポイントをまとめます。 AIアシスタントによるデバッグの向上 Chrome DevToolsの AIアシスタントパネル では、AIとチャット形式でトラブルシューティングが可能です。この機能により、UIコンポーネントのエラー分析やスタイル調整の提案を受けることができ、デバッグ作業が直感的に進められると感じました。 実際にReactアプリでエラーが発生した場合、AIアシスタントがエラー内容を要約し、解決策を提示する様子がデモで紹介されていました。このアプローチはデバッグ効率を大幅に向上させる可能性を感じました。 React Developer Toolsによる拡張性と最適化 React Developer Tools では、レンダー更新のハイライト表示機能が非常に有用です。不要な再レンダーを特定し、パフォーマンス向上につなげられる点が印象的でした。 さらに、Server Componentsのサポートが追加され、クライアントとサーバー側の処理を簡単に区別できるようになったことも、最適化やデバッグ作業の効率化に寄与していると感じました。 ローカルオーバーライド機能での実験と検証 Chrome DevToolsのローカルオーバーライド機能 では、元のコードを変更せずにスタイル調整やAPIレスポンスのモックが可能です。この機能を使うことで、バックエンドが未完成でもフロントエンドのデバッグや検証を進められる柔軟性が魅力的でした。 特にスタイル調整をローカルに保存し、セッションをまたいで変更内容を保持できる点は、デザインの検証作業を効率化できると感じました。 パフォーマンス最適化ツールの活用 Core Web Vitalsの分析機能 は、パフォーマンス改善の具体的な指標を提供し、ユーザー体験の向上に役立つと感じました。また、トレースアノテーション機能を使うことで、チーム内でデータ共有や注釈の付与ができ、パフォーマンス向上施策を共同で進められる点も印象的でした。 3Dレイヤーとアニメーションデバッグ DOM要素の階層構造を視覚的に確認できる 3Dビュー機能 は、インタラクションデザインやアニメーション調整に役立つと感じました。 特にアニメーションインスペクターを使用すると、リアルタイムでタイミングや効果を調整できます。この機能は触り心地の良いWebアプリケーション作成に貢献すると感じました。 まとめ このセッションを聞く前は、そもそもChrome DevToolsにAIアシスタント機能があることを知りませんでした。しかし、AIアシスタントや新しいデバッグ機能、パフォーマンス最適化ツールがWeb開発の効率を大きく向上させることを実感しました。 これらの機能を活用することで、より柔軟で視覚的にアプリケーションを構築できることがすぐに想像できました。計測フロントエンドブロックでの開発でもどんどん活かしていきたいです。 Green Bytes: How Enhancing Web Vitals Contributes to Environmental Sustainability gitnation.com もう一点気になったセッションはZEALのFull Stack Developerの Dimitris Kiriakakis さんのプレゼンテーションでした。 ウェブサイトの最適化は、ユーザー体験の向上だけでなく、環境負荷の軽減にもつながるという視点からの非常に興味深いものでした。このセッションでは、Web Vitalsの改善を通じてウェブアプリケーションのパフォーマンスを向上させ、CO2排出量を削減するための具体的な手法や実践例が紹介されました。 まず、インターネットのカーボンフットプリントについて取り上げられていました。インターネットは世界のCO2排出量の3.7%を占めており、これは航空業界に匹敵する規模だそうです。AI業界の成長に伴い、この数値は今後さらに増加する可能性が指摘されています。ウェブのカーボンフットプリントに影響を与える主な要因として、インフラストラクチャー、データ転送量、エンドユーザーのデバイス使用が挙げられていました。特にページ重量(Page Weight)が大きく影響し、ページサイズが大きいほどネットワーク使用量や電力消費量が増加するという内容は印象的でした。 次に、Core Web Vitalsに関する説明がありました。Googleは2020年にCore Web Vitalsを導入し、ユーザーエクスペリエンスを評価するための指標を標準化しました。具体的には以下の3つの指標が重視されています。 Largest Contentful Paint (LCP) :ページ読み込み速度を測定し、2.5秒以下が良好な体験とされる。 Interaction to Next Paint (INP) :インタラクティブ性を測定し、200ms以下が良好な体験とされる。 Cumulative Layout Shift (CLS) :視覚的な安定性を測定し、0.1以下が良好な体験とされる。 これらの指標を改善することで、パフォーマンスの向上と環境への負荷を減らすことの両方を実現できるという説明があったのですが、これは説得力がありますよね。 具体例として、意図的にパフォーマンスが悪化するように設計されたウェブアプリケーションのケースが紹介されました。Google Lighthouseによる評価では、LCPが13.2秒、CLSが0.367という非常に悪いスコアでした。しかし、以下の最適化を施すことで大幅に改善された事例が示されました。 プレゼンテーションの様子(写真提供:GitNation) 画像最適化 :モバイル向けに小型の画像を生成し、WebP形式に変換。 優先度の設定 :ビューポート内の主要要素を優先的に読み込み、他の要素は遅延ロード。 レイアウトシフトの排除 :安定したレイアウト構造を確保。 これにより、ページ重量は70%削減され、LCPは800ms、CLSは0.1以下という結果を達成したそうです。この事例を通して、具体的な改善策とその効果を明確に理解できました。 さらに、Chromeのパフォーマンスタブを用いたプロファイリングやインタラクションイベントの追跡を活用し、問題点を特定・改善する手法も紹介されました。特にINPスコアの最適化では、重いタスク処理や応答遅延を最小限に抑えることが重要であり、電力消費を抑えながらユーザーエクスペリエンスも向上させることができるとの説明が印象的でした。 このセッションでは、最適化によるCO2排出量削減の具体例も取り上げられていました。あるプラグインのサイズを1KB削減することで、アムステルダムからニューヨークへのフライト5回分のCO2排出を削減できたという報告は、最適化がもたらす環境への影響を強く実感させるものでした。また、ZEALのケースでは、LCPが13秒、CLSが1.775という非常に悪いスコアから、最適化によって劇的に改善された結果が示されていました。 加えて、エコフレンドリーなウェブ開発を支援するツールや手法についても紹介されました。 アセット最適化 :画像圧縮やコードのミニファイ化を通じてデータ量を削減。 グリーンホスティングプロバイダー :再生可能エネルギーを使用するホスティングサービスを選択。 コンテンツデリバリーネットワーク (CDN) :地理的に近いサーバーからコンテンツを配信し、データ転送量を削減。 まとめ これらのアプローチは、持続可能なソフトウェア開発を推進するうえで役立つものであり、企業や開発者にとって今後さらに注目される分野だと感じました。 また、Webのパフォーマンスを向上させることは、自分たちのプロダクトを技術的・ビジネス的に向上させるだけではありません。地球にも優しいという、普段あまり意識していなかった視点からもアプローチできることに気づきました。これにより、非常に良い学びを得ることができました。 今回紹介するのは以上になりますが、他にもアクセシビリティの話や最近フロントエンドフレームワーク界隈でじわじわと人気が上がっている Svelte の話など興味深いセッションがたくさんありました。また、セッションの後に他の参加者と感想を言い合ったり、どのように業務に活かせそうか話したりすることで、一層理解が深まりました。 Day 3 - Workshop 3日目はワークショップでした。私は Kent C. Dodds さんによる「React Future (Server Components and Actions)」のセッションに参加しました。このワークショップでは、まだ公式に安定リリースされていない Server Components と Server Actions についてハンズオン形式で学ぶことができました。Server ComponentsとServer Actionsがどのような役割を果たすのかを、実際にフレームワークを構築しながら理解を深めました。 ワークショップを選んだ理由は、現在計測チームの一部メンバーと共にZOZOMETRYの管理画面を開発しているからです( 関連記事 )。ZOZOMETRYの管理画面は社内メンバー専用のクローズドプロダクトであり、実験的な技術も採用しやすいため、フロントエンドで Next.jsのServer Actions を使用することに決めました。今回、このプロジェクトで初めてServer Actionsに触れたため、さらなる理解を深める目的でこのワークショップに参加しました。 インストラクターのラップトップとマスコットキャラクターのコアラちゃんです。 ワークショップ用のリポジトリをはじめ、オリジナルコアラステッカーなど、至る所がコアラちゃんまみれだったのです。なぜコアラちゃんなのか誰も突っ込んでいなかったため、その謎は解明しないまま今日に至ります。 ワークショップは、まず簡単なアイスブレークと自己紹介から始まりました。自己紹介の内容は、名前、出身地、仕事内容、好きなアイスクリームのフレーバーについてでした。最も人気があったフレーバーは、Cookie Dough(焼く前のCookie生地味)でした。参加者の内訳は、アメリカからが最も多く、次いでヨーロッパ、アジア圏からは私以外に韓国からの参加者がいました。ちなみに、ここでもZOZOMETRYのことを紹介したところ、「クールだ」と言ってもらえました。 ワークショップ内容 Server Components React Server Components(以下、RSC)は、従来のSPAアーキテクチャに代わる新しい手法として注目されています。クライアントとサーバーの役割を効率的に分担し、ストリーミング対応やインタラクティブなUI構築を容易にします。今後の発展に期待しつつ、まずは基本的な仕組みや実装を、作業を通して理解していきました。 Client Components RSCの革新の中心はClient Componentsにあり、これにより新しいアプローチが可能になります。サンプルアプリでは、ボタンをクリックしてテキストを編集し、Enterキーで送信する機能を実装しました。このプロセスを通じて、Client Componentsの重要性や役割について学びました。Client Componentsはインタラクティブな操作を処理するために欠かせない要素であり、ユーザーの操作に応じて即座に反応できる点が特長です。このアプローチにより、ユーザーエクスペリエンスの向上が期待でき、サーバーとクライアントの役割分担を明確にできます。今回のアプリ開発を通じて、Client Componentsが動的な機能やイベント処理を担当する重要な役割を果たすことを理解し、実際にその動作を確認することで、その有用性を実感しました。 Client Router アプリ開発において、スムーズで直感的なユーザーエクスペリエンスを提供するためには、クライアントサイドルーターが欠かせません。ここではClient Routerの役割とその利点について学びました。 Server Actions Server Actionsは、Client Componentsとサーバー間のデータやアクションを効率的にやり取りするための重要な要素です。ワークショップでは、フォームの操作におけるServer Actionsの活用方法を学びました。フォーム送信時に、クライアントからサーバーに対してアクションを呼び出し、その結果を動的に反映させる仕組みです。このように、Server Actionsを使うことで、サーバー側のデータ操作を簡潔に行い、クライアントとのやり取りがシンプルになります。 Server Actionsのポイントは、Client ComponentsからServer Actionsを直接呼び出すことができ、アクションの参照がクライアントに渡されることです。これにより、従来のフォーム送信のような間接的な手法を省き、より直感的で効率的なデータ操作が可能になります。 RSC図解(ワークショップより) 今回のワークショップでは、Server Components、Client Components、Client Router、Server Actionsについて学びました。これらの技術を活用することで、サーバーとクライアント間の役割分担が効率化され、パフォーマンスの向上やユーザーエクスペリエンスの改善が実現可能です。特に、Server Actionsを使ったデータのやり取りや、Client ComponentsによるインタラクティブなUIの実装は、今後のアプリケーション開発において非常に有用な技術です。これらの技術が今後どのように進化していくのか、非常に楽しみです。 本来数日間にわたって行う内容を1日で詰め込んだため非常にハードでしたが、周りの参加者と助け合いながら何とか乗り切ることができました。参加者の多くはフレンドリーで、協力して課題を解決していく様子は学生時代のようで楽しかったです。また、インストラクターとも距離が近く、カンファレンスの時よりもリラックスして話ができ、裏話なども聞くことができました(2024年は登壇しすぎてしんどかった等)。 ワークショップが終わった後は、他の参加者たちと「来年も会えるといいね」と言い合いながら、別れを惜しみました。 ワークショップの様子です。 少人数での開催だったため、非常にアットホームでフレンドリーな雰囲気でした。また、インストラクターは課題を完成させることよりも、他の参加者と多くの会話をすることを推奨していました。 余談ですが、写真を見ると分かるように半袖の参加者もいます。この日は最高気温が11度だったにもかかわらず室内は冷房が7度に設定されており、エアコンの風が髪をそよがせるほどでした。その結果、私は体調を崩しました。 お昼ご飯はアメリカンメキシカン、いわゆるTex-Mexでした。メキシカンが大好きなので嬉しかったです。 イベント全体を通じて感じたこと ゲットしたノベルティたち。 今回のカンファレンスは、国内外を通じて私にとって初めての参加となりました。これまで「ニューヨークの人々は西海岸の人々に比べて冷たい」と耳にしていたため、どのような雰囲気なのか少し緊張していました。しかし実際には、多くの参加者がとてもフレンドリーで交流を楽しむことができました。 また、最近ではアメリカの若者にとって日本が人気の旅行先となっているようで、日本を訪れた経験のある参加者にも多く出会いました。参加者層を見ていると、東海岸からのアメリカ人が最も多く、次いでカナダの東部からの参加者が目立ちました。一方で、西海岸からの参加者は意外に多くなく、ヨーロッパからの参加者も限定的でした。しかし、とあるイタリア人エンジニアによると、ヨーロッパからもそれなりに参加者がいたようです。アメリカやカナダ以外では、ESTAを利用して参加できる国からの参加者が多いように思いました。ちなみに、日本から片道13時間ほどかけてニューヨークまでやってきたと言うととても驚かれました。 セッションはオンラインでも視聴可能なため、「技術を学ぶ」という観点では動画配信でも十分に知識を得られるかもしれません。しかし、現地で世界中から集まったエンジニアたちと直接対話することで知識の吸収にとどまらず、彼らの働く環境やマインドセット、バックグラウンドについても知ることができました。自分の中の『当たり前』が、他の人にとっては異なることを実感できる貴重な体験は、視野を広げる素晴らしい機会となりました。 今回のカンファレンスを通じて著名なエンジニアや世界的企業に所属するエンジニアと交流できたことで、技術知識をアップデートしただけではなく、大きな刺激を受け日々の開発へのモチベーションも向上しました。そして何よりも非常に楽しい時間を過ごすことができました。今後も機会があれば今回のご縁を大切にし、現地で再会できることを楽しみにしています。 最後に 最後に、計測システム部フロントエンドブロックでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
ZOZO開発組織の2025年1月分の活動を振り返り、ZOZO TECH BLOGで公開した記事や登壇・掲載情報などをまとめたMonthly Tech Reportをお届けします。 ZOZO TECH BLOG 2025年1月には、前月分のMonthly Tech Reportを含め5本の記事を公開しました。その中でも特に注目度の高かった記事をピックアップしてご紹介します。 1月21日に公開した「 フロントエンドテストの正解って?FAANSにおけるテスト戦略の振り返りとこれから 」は、リアルな現場感を綴った記事です。2025年1月第4週のおすすめエントリーとして、 はてなブログのXアカウントにも取り上げてもらいました 。 Webフロントエンドにおけるテストには様々な論点がありますが、FAANSの事例を通じて、何か得られるものがあればうれしいですね。 techblog.zozo.com また、1月最終日に公開した「 BigQueryのアンチパターン認識ツールで独自のSQLリンターを開発しました 」では内定者アルバイトスタッフの成果物を紹介しています。 取り組む中で見つけたエラーに対してPull requestを送ってマージされることで、問題の発生を防げるようになりました。ZOZOではこのような取り組みを応援、そして歓迎しています! techblog.zozo.com ZOZO DEVELOPERS BLOG EC基盤開発本部 SRE部の三神と亀井による、ZOZOTOWNにおけるSREの魅力や日常を紹介する記事を公開しました。SRE部の組織構成から日々の業務、そして今後の展望についても触れています。ZOZOTOWNのSREに興味のある方は、ぜひご一読ください。 technote.zozo.com 登壇 LODGE XR Talk Vol.23 1月17日に開催された『 LODGE XR Talk Vol.23 』で、技術戦略部の諸星( @ikkou )が「CES 2025 Report」と題して1月上旬に参加したCES 2025でのXRの動向について登壇しました。 登壇後に公開したCES 2025のイベントレポート記事では、XRに限らずFashion TechやBeauty Techについても触れています。あわせてご覧ください。 techblog.zozo.com JR西日本・ファーストリテイリング・ZOZOが語るビジネス要件を踏まえたデータ基盤の構築 1月24日に開催された『 JR西日本・ファーストリテイリング・ZOZOが語るビジネス要件を踏まえたデータ基盤の構築 』で、データ基盤ブロック ブロック長の奥山( @pokoyakazan )が「 ZOZOを支えるリアルタイムデータ連携基盤の歴史とビジネス貢献 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 明日1/24(金) 12:00~13:15にオンラインで開催される『JR西日本・ファーストリテイリング・ZOZOが語るビジネス要件を踏まえたデータ基盤の構築』にデータシステム部の奥山 @pokoyakazan が登壇します🎙️ お気軽にご参加ください! https://t.co/OVMWx370aT #data_findytools — ZOZO Developers (@zozotech) 2025年1月23日 speakerdeck.com 掲載 教育新聞 昨年12月15日に開催した「 Girls Meet STEM〜ITのお仕事を体験しよう〜 」について、技術戦略部の長澤( @wiroha )が取材を受けた記事が、「 教育新聞 」に掲載されました。 www.kyobun.co.jp エンジニアtype エンジニアtypeが運営する音声コンテンツ『 聴くエンジニアtype 』の配信100回突破を記念した過去のおすすめ回を特集した記事に、CTOの瀬尾( @sonots )登場回が紹介されました。 type.jp 瀬尾が登場した第1回のテーマは「 エンジニアとして成長するために新人時代にするべきことは? 」でした。こちらは記事としても公開されています。未見の方はぜひチェックしてみてください。 type.jp AdverTimes. 昨年12月18日にAI・アナリティクス本部の川田が登壇した「 宣伝会議AI研究会 」の様子が、「 AdverTimes. 」に掲載されました。 www.advertimes.com その他 2025年3月期 第3四半期決算発表 1月31日に2025年3月期 第3四半期決算を開示しました。詳細は以下のリンクにある開示資料をご確認ください。 corp.zozo.com 以上、2025年1月のZOZOの活動をお届けしました! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、データシステム部データ基盤ブロックの奥山( @pokoyakazan )です。普段は全社データ基盤の開発・運用を担当しており、最近ではZOZO全体のデータガバナンス強化にも取り組んでおります。本記事ではCloud Composer上に構築しているデータマート集計基盤でdbtのモデル更新も行えるようにした事例についてご紹介します。 目次 はじめに 目次 背景 データマート集計基盤 dbt導入 データマートの使い分け dbt導入にあたっての課題 モデルごとに自動リトライができない 依存関係による待ち合わせ制御ができない データマート集計基盤へのdbt導入 Airflow Dagの設計 dbtデータマート更新処理の実装 データマートごとのタスクグループ作成 1. モデル情報を保持するクラスの定義 2. manifest.jsonの読み込みとモデル情報の取得 3. タスクグループの作成 dbtモデル間の依存関係を定義 dbtデータマート→dbtデータマート ソースシステム→dbtデータマート dbtデータマート→SQLデータマート Elementaryを使った実行履歴・テスト結果の可視化 まとめ 背景 データマート集計基盤 ZOZOでは、データ基盤利用者が作成したSQLファイルに記述されたクエリによって日々更新されるBigQueryのテーブルをデータマートとして管理しています。とても活発に利用されており、2025年2月現在1,100を超えるデータマートが存在します。そしてこれらのデータマートを更新するジョブを管理するシステムをデータマート集計基盤と呼んでおり、 Apache Airflow のマネージドサービスである Cloud Composer 上に構築しています。Cloud Composerの導入事例については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com dbt導入 データ利活用が進む中で、データ基盤におけるデータガバナンスを強化していこうという取り組みが始まりました。そこでより品質担保されたデータマートを提供することを目的として、複数のデータモデリングツールを比較検討した結果、 dbt を導入することにしました。dbtの選定理由・導入経緯については以下の記事で紹介しているのでぜひご覧ください。 techblog.zozo.com なお今回の記事はこちらの記事の続編となり、実際にdbtをシステムに組み込んでいくにあたっての過程や方法について紹介していきます。 データマートの使い分け 前提として、今回のdbt導入にあたってすでにデータマート集計基盤で日々更新されている1,100を超えるデータマートをすべてdbtでモデリングし直す方針は取っていません。SQLを書くだけで完結する既存の仕組みは利便性が高く、データ基盤の利用促進に繋がりますし、ビジネス部門の方々を含む全ての利用者に1からdbtを学習してもらうのは現実的でないと判断したためです。そこで、以下2つのデータマートを使い分けることにしました。 SQLデータマート: これまでのSQLファイルで更新されるデータマート レポーティング用途 dbtデータマート: dbtによって更新されるデータマート 集計定義を統制して品質担保 dbt導入にあたっての課題 dbtをそのまま単体で導入するだけでは、運用面で以下のような課題がありました。 モデルごとに自動リトライができない 依存関係による待ち合わせ制御ができない 1つずつ見ていきます。 モデルごとに自動リトライができない dbtでは dbt run コマンド1つで依存関係を考慮しながら全モデルを一括更新できます。そのためサーバ上のcronやGitHub Actionsから簡単に実行が可能です。ただしこの方法では、途中モデル更新が失敗した際に効率的な再実行ができないという課題がありました。 例えば、以下のような依存関係を持つ dbt_model1 から dbt_model5 を dbt run で一括更新するとします。 ここで、 dbt_model1 ~ dbt_model3 の更新は成功し、 dbt_model4 の更新で失敗した場合を考えます。 この時、GitHub Actionsジョブ内で dbt run を実行するstepを再実行する必要があります。ただし再実行は全モデルが対象となるため dbt_model1 ~ dbt_model3 の更新も最初からやり直しとなり、無駄な時間とリソースが発生してしまいます。 依存関係による待ち合わせ制御ができない dbtモデルにはソースシステムから連携される一次テーブルを参照するものもあり、そういったモデルは依存するソースシステムのデータ連携が完了するまで更新開始を待つ必要があります。例えばGitHub Actionsでこの処理を実現する際は待ち処理用のstepを用意することになるかと思います。この場合、全てのソースシステムからの連携完了を確認するまで dbt run は実行できず、結果として無駄な待ち時間が発生します。依存するソースシステムの連携が終わり次第、対象モデルを即時更新していく仕組みが理想です。 データマート集計基盤へのdbt導入 上記の「モデルごとの自動リトライ」や「依存関係による待ち合わせ制御」といった機構は、すでにデータマート集計基盤で実装済みです。そのためSQLデータマートと同様、Cloud Composerからdbtデータマートを更新する仕組みにしました。また、「SQLデータマートからdbtデータマートを参照したい」という要件も挙がっていたため、1つのAirflow DagでSQLデータマートとdbtデータマートの両方を更新できるよう設計しています。 補足となりますが、Airflow上からdbtを実行できる Cosmos というOSSツールがあります。しかし、上記のSQLデータマートからdbtデータマートへの依存に関する要件や、今後発生するビジネス要件に柔軟に対応する必要があることを考慮し、OSSではなく自分達で内製することにしました。 Airflow Dagの設計 Airflow Dagからdbtモデルを更新していくにあたってのポイントは「1つのAirflowタスクごとに1つのdbtモデルを更新する」という点です。dbtでは --select オプションを使うことで、特定のモデルやタグ、その他の条件(例えば、モデルの状態や依存関係など)によって更新対象のモデルを選択できます。そこでdbtモデルごとにAirflowタスクを作成し、 --select オプションを使って対象のモデルのみを更新するようにしました。 dbt run --select " ${ 対象dbtモデル名 } " 依存関係の解析や待ち合わせ制御をAirflowに任せることで、タスクの失敗時にはそのタスクのみを再実行でき、他のタスクに影響を与えずに処理を続けることができます。具体的には、データマート単位でタスクグループを作成し、それぞれのタスクグループ内でデータマートの更新処理とデータ品質チェックを行う2つのタスクを定義しています。 データマートの更新処理( update_datamart タスク): dbtデータマートの場合 dbt run を実行 データ品質チェック( data_quality_check タスク): dbtデータマートの場合 dbt test を実行 SQLデータマートの場合Dataplexを利用(本記事では割愛) また、全てのdbtデータマートタスクグループの処理完了後に dbt_test_warning タスクを実行します。 データ品質チェックについて詳しく見ていきます。データ品質チェックの方法は大きく分けて2種類あります。 Errorデータ品質チェック: 各dbtデータマートの更新直後に data_quality_check タスクで実行 Warningデータ品質チェック: 全てのdbtデータマートの更新完了後に dbt_test_warning タスクで実行 dbtでは severity という設定で、データ品質チェックの重要度を error または warn から設定できます。「Errorデータ品質チェック」では、致命的なデータ品質の問題を検出するため、 severity:error に設定したテストを以下のコマンドで実行します。 dbt test --select " ${ 対象dbtモデル名 } ,config.severity:error " このチェックはデータマートごとに実行され、品質に問題が見つかった場合、後続のタスクを停止します。一方、「Warningデータ品質チェック」では、 severity:warn に設定したテストを以下のコマンドで実行します。 dbt test --select " config.severity:warn " select でモデルを指定せず全てのdbtデータマートに対して一括でテストを行っており、Warningが発生しても後続のタスクはそのまま実行されます。また、品質に問題が見つかってもSlack通知のみ送るようにしています。このように、ErrorチェックとWarningチェックを適切に分けることで、重要なデータ品質問題は即時で対応し、Warningレベルの課題は効率的にモニタリングすることが可能となります。 dbtデータマート更新処理の実装 実際にCloud Composer(Airflow)からdbtモデルを更新するコードについて見ていきます。dbtデータマートの更新処理における実装のポイントは主に2つあります。 データマートごとのタスクグループ作成 dbtモデル間の依存関係を定義 これらの実装は、dbtコマンド実行後に生成される manifest.json を解析することで行っています。 manifest.json は、dbtプロジェクトのメタ情報を保持するファイルで、以下のような情報が記載されています。 各dbtモデルの詳細(モデル名、ファイルパスなど) モデル間の依存関係 dbtテストなどの情報 データマートごとのタスクグループ作成 1. モデル情報を保持するクラスの定義 manifest.json を読み込む前に、前準備として各dbtモデルの情報を保持するための DbtModel クラスを定義します。このクラスでは、テーブル名・依存先モデル・ユニークIDなど、 manifest.jsonに記載されているモデルのメタ情報 をプロパティとして管理します。 class DbtModel (): def __init__ (self, project_id, unique_id, dataset, table, depends_on_models): self._project_id = project_id self._unique_id = unique_id self._dataset = dataset self._table = table self._depends_on_models = depends_on_models @ property def project_id (self): return self._project_id @ property def unique_id (self): return self._unique_id @ property def dataset (self): return self._dataset @ property def table (self): return self._table @ property def depends_on_models (self): return self._depends_on_models def table_id (self): return f '{self._project_id}.{self._dataset}.{self._table}' 2. manifest.jsonの読み込みとモデル情報の取得 次に manifest.json をロードして各モデルの詳細情報を取得し、取得した情報から DbtModel をインスタンス化していきます。その後、インスタンス化した DbtModel オブジェクトを dbt_models リストに追加していきます。 with open ( 'target/manifest.json' ) as f: manifest_dict = json.load(f) dbt_models = [] for node in manifest_dict[ "nodes" ].keys(): if node.split( '.' )[ 0 ] == "model" : model_conf = manifest_dict[ "nodes" ][node] dbt_model = DbtModel( project_id=model_conf[ 'database' ], unique_id=model_conf[ 'unique_id' ], dataset=model_conf[ 'schema' ], table=model_conf[ 'name' ], depends_on_models=model_conf[ 'depends_on' ][ 'nodes' ], ) dbt_models.append(dbt_model) 3. タスクグループの作成 dbt_models リストをループし、各モデルに対応するタスクグループを作成していきます。タスクグループ内には、先述の update_datamart タスクと data_quality_check タスクを定義しています。 # タスクを格納する辞書 task_dict = {} for dbt_model in dbt_models: model = dbt_model.table with TaskGroup(group_id=model) as task_dict[model]: dbt_run_command = f 'dbt run --select "{model}"' update_datamart = BashOperator( task_id= 'update_datamart' , bash_command=dbt_run_command, ) dbt_test_command = f 'dbt test --select "{model},config.severity:error"' data_quality_check = BashOperator( task_id= 'data_quality_check' , bash_command=dbt_test_command, ) update_datamart >> data_quality_check dbtモデル間の依存関係を定義 続いて、タスク間の依存関係をどのように定義していくかを紹介します。依存関係は以下の3つに分けて考えます。 dbtデータマート→dbtデータマート ソースシステム→dbtデータマート dbtデータマート→SQLデータマート これらの依存関係の定義方法について、それぞれ詳しく見ていきます。 dbtデータマート→dbtデータマート まず、dbtデータマート同士の依存関係について紹介します。 manifest.json 内の各モデル情報には depends_on_models というリストが含まれており、このリストには依存先となるモデルが格納されています。この情報を元にAirflowでタスク間の依存関係を定義していきます。 # 先ほど作成したdbt_modelsリストをループ for dbt_model in dbt_models: # dbtモデルが依存するnodeのunique_idでループ for depends_on_node_unique_id in dbt_model.depends_on_models: # 依存先nodeがmodelの場合依存関係定義 if depends_on_node_unique_id.split( '.' )[ 0 ] == "model" : depends_on_model = depends_on_node_unique_id.split( '.' )[- 1 ] # task_dict: タスクを格納する辞書 task_dict[depends_on_model] >> task_dict[dbt_model.table] depends_on_models リストをループし、各モデルの依存先を確認していきます。依存先がdbtモデル( unique_id の先頭が model )の場合に、Airflowの >> 演算子を用いて依存関係を定義しています。 ソースシステム→dbtデータマート 次に、ソースシステムからdbtデータマートへの依存関係について紹介します。まず、source情報を保持するために DbtModel と同様 DbtSource クラスを用意します。そして各dbtモデルの depends_on_models を調べていきます。依存先がdbtのsource( unique_id の先頭が source )である場合、そのsourceの情報を取得し、取得した情報から DbtSource をインスタンス化します。その後、インスタンス化した DbtSource オブジェクトを sources リストに追加していきます。 # 待ち処理を行うSourceのリスト sources = [] # 依存関係リスト dependencies = [] # dbtモデルごとのループ for dbt_model in dbt_models: # dbtモデルのunique_idを取得 dbt_model_unique_id = dbt_model.unique_id # dbtモデルが依存するモデルのunique_idでループ for depends_on_node_unique_id in dbt_model.depends_on_models: # 依存するモデルがSourceの場合、情報を取得 if depends_on_node_unique_id.split( '.' )[ 0 ] == 'source' : source_conf = manifest_dict[ "sources" ][depends_on_node_unique_id] unique_id = source_conf[ 'unique_id' ] project_id = source_conf[ 'database' ] dataset = source_conf[ 'schema' ] table = source_conf[ 'name' ] # DbtModelクラス同様、Source用クラスで情報を保持 dbt_source = DbtSource(project_id, unique_id, dataset, table) # DbtSourceインスタンスごとにリストに追加 sources.append(dbt_source) # 依存関係リストに追加 dependencies.append({ 'before' : dbt_source.table, 'after' : dbt_model.table}) そして DbtSource オブジェクトごとに待ち処理用のタスクを定義し、最後に依存関係を貼っていきます。 # Sourceのリストから待ち処理を行うタスクを生成 for source in sources: task_dict[source.table] = PythonOperator( task_id=f 'wait_{source.table}' , # ソースシステムの待ち処理を行うタスク python_callable=_wait_source_created, ) # 依存関係リストから依存関係を定義 for dependency in dependencies: before = dependency[ 'before' ] after = dependency[ 'after' ] task_dict[before] >> task_dict[after] dbtデータマート→SQLデータマート 最後に、dbtデータマートからSQLデータマートへの依存関係について紹介します。SQLファイルを解析し、 FROM 句や JOIN 句の後に記載されているテーブルIDを取得する処理は既にデータマート集計基盤で実装済みです 1 。そのため、この処理で得られた参照先のテーブルIDとdbtデータマートのテーブルIDを比較し、一致した場合に依存関係を定義していきます。 # SQLデータマートごとのループ for datamart in sql_datamarts: """ SQLを解析してFROM, JOINの後にくるテーブルIDを取得し、 取得したテーブルIDをdepends_on_table_idsリストに格納する処理 datamart: DbtModelクラス同様、SQLデータマート用クラスで情報を保持している """ # FROM, JOINの後にくるテーブルIDでループ for depends_on_table_id in depends_on_table_ids: # dbtモデルごとのループ for dbt_model in dbt_models: # FROM, JOINの後にくるテーブルIDとdbtモデルのテーブルIDを比較 # 一致した場合、依存関係を定義 if depends_on_table_id == dbt_model.table_id(): task_dict[dbt_model.table] >> task_dict[datamart.table] Elementaryを使った実行履歴・テスト結果の可視化 最後にdbtの運用におけるTipsとして、実行履歴やテスト結果の可視化について紹介します。dbtでの問題発生時のSlack通知や dbt test の履歴管理のために、dbtのオブザーバビリティツールである Elementary を使っています。 Elementary には edr というCLIコマンドがあり、主に2つのサブコマンド monitor と send-report が重要です。 edr monitor はdbtコマンドで発生したErrorやWarningをSlackなどに通知するコマンドで、dbtデータマートのタスクグループ内のタスクが失敗した際に実行されます。具体的には、Airflowの on_failure_callback 機能を使ってタスク失敗時にのみ実行される関数内で edr monitor を実行します。また、 dbt_test_warning タスクの後にも edr monitor を実行することで、データ品質チェックのWarning通知も飛ばしています。一方、 edr send-report は、これまでの実行履歴を元にモニタリング用ダッシュボードを作成するコマンドで、全てのdbt関連タスクの完了後、最後に実行してダッシュボードを更新します。 Elementary を活用することで、実行履歴やデータ品質チェックの結果を可視化し、ErrorやWarningに迅速に対応できるようになります。 まとめ 本記事では、データマート集計基盤でdbtのモデル更新も行えるようにした事例について紹介しました。利用者がSQLファイルを書くだけでデータマートを更新できる既存の仕組みを残しつつ、集計定義を統制して品質担保したいデータマートはdbtでモデリングしていく方針を取りました。今後もデータの品質の向上やガバナンス強化のためにデータマート集計基盤を改善していく予定です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 「Cloud Composerにデータマート集計基盤を移行しました」の「各マートのSQLファイルからマート間の依存関係グラフの作成」を参照ください ↩
はじめに こんにちは、データサイエンス部データサイエンス2ブロックの Nishiyama です。我々のチームでは、AIやデータサイエンスを活用したプロダクトの開発ために、研究開発に取り組んでいます。我々のチームの具体的な業務については、以下の記事を参考にしてください。 techblog.zozo.com 本記事では、レビューパトロールの業務時間を67.7%削減したガイドライン違反検出ツールの開発について述べます。社内で特定の部署が抱える課題を解決し、業務効率を上げるツールを開発する方の一助になると幸いです。 目次 はじめに 目次 ガイドライン違反検出ツール 背景 作成した理由 課題の原因 課題の特定方法 課題の解決方法 技術選定 LLMを用いたガイドライン違反検出ツール開発 実験 評価 コスト 開発で徹底したこと さいごに ガイドライン違反検出ツール 開発したガイドライン違反検出ツールは、LLMを用いてZOZOTOWN上のレビューをパトロールし、違反を検出します。具体的には、以下に示すパイプラインを開発しました。 ガイドライン違反検出ツールのパイプライン図 パイプラインは、バッチ処理として実行されます。以下は詳細なステップです。 Cloud SchedulerはCloud Functionsをトリガーする Cloud FunctionsはVertex AI Pipelinesをキックする BigQueryから対象期間のレビューを抽出する Cloud Storageからガイドラインを取得する 3と4で得られたレビューとガイドラインをガイドライン違反検出ロジックへ入力する 違反検出ロジックは、違反可能性が高いレビューと違反理由を出力する 違反可能性が高いレビューと違反レビューをGoogle スプレッドシート(以下、シートと呼ぶ)に書き出す シートのURLを取得し、Slackへ通知する 以降は、ガイドライン違反検出ツールを作成した理由と、技術選定について述べていきます。 背景 2023年にZOZOTOWNは レビュー機能 を実装し、ユーザーは、ZOZOTOWNで購入した商品へレビューを投稿することやレビューを閲覧できるようになりました。また、健全なサイト運営のためのレビュー投稿ルールとして レビューガイドライン を導入しパトロール業務が発生しました。パトロール業務は、投稿されたレビューに対して、レビューガイドライン違反の有無を判定する業務です。パトロールでレビューガイドライン違反と判断されたレビューは、ガイドラインに従ってZOZOTOWN上から取り下げる対応をします。 ZOZOTOWN上のレビュー例 作成した理由 ガイドライン違反検出ツールは、パトロール業務を担当する部署が抱えていた、次の2つの課題を解決する価値が高いため開発しました。 現在、パトロール業務にかける時間や担当者が多い 将来、パトロール業務にかける時間や担当者が増加する 上記の課題を解決することで、パトロール担当部署は、これまでパトロール業務にかけていた時間や担当者を将来的にも別の業務に割り当てることができます。よって、解決する価値が高いと判断しました。 課題の原因 まず、パトロール業務にかける時間や担当者が多い理由は、一件ずつ目視で確認していたからです。以下にパトロール業務のフローを示します。 対象の期間のレビューを集計しシートに書き出す レビューのタイトルと内容がガイドラインに違反しているか目視で照らし合わせて確認する ガイドライン違反の有無と理由をシートに記載する 特に、パトロール業務のフロー2で照らし合わせるガイドラインは、30項目以上あります。そのため、パトロール業務の時間や担当者数の増加につながっています。 次に、将来パトロール業務にかける時間と人数が増える理由は、投稿されるレビュー数の増加が考えられるからです。要因として以下が考えられます。 レビュー機能の認知向上 商品数の増加 キャンペーンの実施 パトロール業務フローを見ると、投稿されるレビュー数の増加は、そのままパトロール対象のレビュー数の増加につながることがわかります。 課題の特定方法 上記の課題と原因を特定した方法は、関係者へのヒアリングです。以下のようなヒアリングをして、パトロール担当者の業務を理解し課題の深掘りをしました。 パトロール業務の目的と内容 パトロール業務に時間と人がかかる理由 ヒアリング内容から、課題の解決された状態を想定して、リリース基準を作成しました。 課題の解決方法 パトロール業務の品質を担保した上で課題を解決するために、半自動化の運用を採りました。半自動化は、ガイドライン違反検出ツールの出力を、パトロール担当者が目視確認する運用です。半自動化の運用にした理由は、ガイドライン違反検出ツールのみで、違反を確定させることが難しかったからです。具体的には、商品画像を参照しなければ分からない商品不良や人間の判断に委ねるべき曖昧なレビューが該当します。下記にガイドライン違反ツールを用いたパトロール業務のフローを示します。 ガイドライン違反検出ツールが対象の期間のレビューを入力として違反可能性が高いレビューと違反理由をシートに書き出す パトロール担当者は違反可能性の高いレビューがガイドラインに違反しているか目視で照らし合わせて確認する ガイドライン違反の有無と理由をシートに記載する ガイドライン違反検出ツールは、対象期間のレビューを入力として、違反可能性が高いレビューを出力します。違反可能性が高いレビュー数は、対象期間のレビュー数と比較して、68.5%少ない量になります。したがって、パトロール業務を担当する部署が抱えていた課題を以下の2点において解決していると言えます。 現在、対象期間のすべてのレビューと比較して68.5%少ない違反可能性が高いレビューの目視確認で済むため、パトロールにかける時間や担当者を削減できる 将来、投稿されるレビューが増加した際も、違反可能性が高いレビューのみ目視確認するため、時間と担当者の増加を抑えることができる 技術選定 ガイドライン違反可能性の高いレビューを検出するために、LLMのモデルとしてOpenAI APIのgpt-4-1106-previewを採用しました。OpenAIのGPT-4 APIを採用した理由は2点あります。1点目は、少量のデータセットに対してIn-Context Learningすることである程度の検出力が見込めたからです。2点目は、In-Contex Learningによりガイドライン違反基準を素早く柔軟に変更できるためです。素早く柔軟な違反基準の変更がメリットになる理由は、違反基準に主観を含む言語化の難しい曖昧なレビューが存在しているためです。技術選定時に比較・検討した手法は以下です。 LLM ナイーブなテキストフィルタリング モデルのフルスクラッチ・ファインチューニング 比較・検討結果について述べます。ナイーブなテキストフィルタリングは、違反可能性の高いレビュー検出の精度が低くなりました。なぜなら、同様のテキストが含まれる文章であっても、使用される文脈によって、ガイドライン違反の有無が異なるためです。次に、モデルのフルスクラッチ・ファインチューニングは、大きなデータセットをアノテーション段階から構築する必要があったため見送りました。 LLMを用いたガイドライン違反検出ツール開発 LLMを用いたツール開発の戦略は、 Optimizing LLM Accuracy に従いました。promptの書き方については、 Best practices for prompt engineering with the OpenAI API を参考にしました。特に参考にした点は以下です。 stepを明示的に示す Few-shot promptingを与える 期待する出力を明示する 各項目について、具体例を交えて、説明します。 1点目の「stepを明示的に示す」では、細かくstepを分け明示的に推論過程を与えました。加えて、Zero-shot Chain-of-Thoughtも使用しました。具体的な例を以下に示します。 prompt: str = f """ step1: ガイドライン項目をよく読んで、理解します。 step2: レビュー内容をガイドラインと1つ1つ照らし合わせて違反を検出します。 step3: 違反の有無と理由をあなたの考えも合わせて出力してください。 Let's think step by step """ 2点目の「Few-shot promptingを与える」では、ガイドライン違反検出の精度が低い項目に絞って使用しました。Few-shot promptingは、LLMに少数の例を明示的に与える方法です。具体的な例を以下に示します。 prompt: str = f """ 違反の有無と理由をあなたの考えも合わせて出力してください。 {guidelines[i]}の違反例: {violation_examples} """ # guideline[i]: 違反検出精度が低いガイドラインの本文 # violation_example: 違反例 3点目の「期待する出力を明示する」では、JSON formatを明示的に与えました。理由は2点あります。1点目は、出力を通し番号にすることで、input tokensより3倍高いoutput tokensの料金を抑える狙いがあったからです。2点目は、 response_format={"type": "json_object"} とした場合でも期待するJSON formatではない場合があったからです。 prompt: str = f """ あなたの出力は以下の出力フォーマット例に従ったjson formatです。 出力フォーマット例: 1. 違反がある場合: {{"違反あり" : "通し番号"}} 2. 違反が複数ある場合: {{"違反あり" : "通し番号,通し番号")}} 3. 違反が無い場合: {{"違反なし" : ""}} """ 実験 定量評価のための実験は、学習データセットに対してPrompt Engineeringを行いprecision, recall, f1-scoreを確認し、エラー分析をしました。データセットは、パトロール業務担当者にアノテーションを依頼し少量ずつ構築しました。 定性評価のための実験は、2つあります。1つ目は下記のようにグループを分け、ガイドライン違反を目視確認する実験です。この実験の目的は、ガイドライン違反ツールの出力が、目視確認とどの程度異なるか検証することです。2つ目は、False Negativeの質に対する実験です。この実験の目的は、ガイドライン違反検出ツールが許容できない見逃しをしていないかの確認です。なぜなら、半自動化の運用上、False Negativeのレビューは、担当者による目視確認がされないままZOZOTOWN上に掲載されるためです。 グループ タスク Aグループ 全てのレビューを目視確認する Bグループ ガイドライン違反可能性の高いレビューのみ目視確認する 評価 リリース基準を超えた時点での定量評価と定性評価について述べます。定量的な評価は以下のようになりました。precisionがやや低いため、目視確認する量が増えています。一方recallは高いツールとなっていることが分かります。 手法 / 評価指標 precision recall f1-score ガイドライン違反ツール 0.75 0.934 0.8319 定性的な評価は、AグループとBグループで出力の差異がほとんどない結果になりました。また、False Negativeの質についても、問題がない範囲であることを確認しました。 コスト ガイドライン違反検出ツールにかかる費用は、2つあります。1つ目がgpt-4-1106-previewの料金で $ 0.022/1レビューです。2つ目がGoogle Cloudの料金で、dev, stg, prd環境を合わせて $ 4.11/月です。 開発で徹底したこと ガイドライン違反検出ツールの開発で徹底したことは、パトロール担当部署の課題を解決することです。そのために、パトロール担当部署と連携を取りました。具体的には、定例ミーティングへの参加と定性評価と実験です。定例ミーティングへ参加することで、以下のメリットがありました。 開発着手時から後のユーザーとなる現場の声をツールに反映できる 現場の課題の優先度を鑑みて、開発する機能の優先度を揃えたスケジュール管理ができる 開発者がパトロール担当者から直接良い/悪いフィードバックを受けられる 認識の齟齬によるプロジェクトの手戻りが無い 定性評価と実験では、実験の設計段階から双方向に会話をして取り決めることができました。加えて、普段からコミュニケーションを取っていたため、実験が終わり次第評価・フィードバックというサイクルを早めることができました。私とパトロール担当部署を含めた関係者の間で課題を解決することを共有し、同じ方向を向けていたことは、ガイドライン違反検出ツールの質を高めたと感じます。 さいごに 本記事では、ガイドライン違反検出ツールの開発を紹介しました。ガイドライン違反検出ツールの導入により、業務時間を67.7%削減、チェック件数を68.5%削減しました。課題解決のためのツール作成を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、株式会社ZOZOで25卒の内定者アルバイトをしている村井です。この記事では業務で取り組んでいる、BigQueryで使うSQLのリンターの作成方法について紹介します。 目次 目次 課題と解決策 課題 解決策 BigQueryのアンチパターン認識ツール ミニマムな使い方 日本語がSQL内に含まれている際の問題 アンチパターンを定義する リンターとしてBigQueryのアンチパターン認識ツールを使用する際に生じる課題と解決策 構成 APIサーバ化 Chrome拡張 動作例 まとめ 課題と解決策 課題 社内では様々なチームがSQLを書いており、動作はするものの良くない書き方をしている場合があります。そういった構文を検知して、前もって修正する必要があります。 解決策 BigQueryのコンソールで入力されたSQLの不正構文を検知、修正案を提示できるようにしました。 BigQueryのアンチパターン認識ツール BigQueryのアンチパターン認識ツール とはGoogleが作成しているBigQueryのアンチパターンを教えてくれるツールです。 自分でアンチパターンを定義せずに使用した場合、以下のアンチパターンを検知して教えてくれます。 Selecting all columns 以下のように、アスタリスクによって全てのカラムが選択されている際のアンチパターンです。 SELECT * FROM `project.dataset.users`; SEMI-JOIN without aggregation DISTINCTのないサブクエリでINを使用しているときに発生します。 SELECT u.name FROM `project.dataset.users` u WHERE u.id IN ( SELECT id FROM `project.dataset.orders` o WHERE o.status = ' shipped ' ); Multiple CTEs referenced more than twice CTEが複数参照されているときに発生します。 WITH a AS (SELECT col1 FROM `project.dataset.table1`), b AS (SELECT col2 FROM a), c AS (SELECT col1 FROM a) SELECT a.col1, b.col2, c.col1 FROM a, b, c; Using ORDER BY without LIMIT ORDER BY句をLIMIT無しで使用したときに発生します。 SELECT name, age FROM `project.dataset.employees` ORDER BY age DESC ; Using REGEXP_CONTAINS when LIKE is an option LIKE句で十分表現でき、正規表現を使う必要がない場合に発生します。 SELECT username FROM `project.dataset.users` WHERE REGEXP_CONTAINS(username, r ' .*admin.* ' ); Using an analytic functions to determine latest record 分析関数を用いて最新のレコードを特定するときに発生します。以下のコードはrow_numberを使用していますが、ORDER BYとLIMITを使って書き直すことができます。 SELECT id, fare FROM ( SELECT id, fare, row_number() over(partition by id order by fare desc ) rn FROM `project.dataset.table1` ) WHERE rn = 1 ; Convert Dynamic Predicates into Static Dynamic PredicateをStatic Predicateに変えるとパフォーマンスが向上するかもしれないときに発生します。以下のコードは、WHERE句に含まれるサブクエリが動的に条件を生成します。 SELECT * FROM `project.dataset.users` u WHERE u.id IN ( SELECT id FROM `project.dataset.customers` WHERE region = ' US ' ); Where order, apply most selective expression first WHERE句の中のフィルタ条件が不適切な順序であるときに発生します。以下のコードには price > 1000 と category = 'electronics' のフィルタがあります。この場合選択性が高いのは category = 'electronics' なので price > 1000 より前に書くべきであるということです。 SELECT id, name FROM `project.dataset.products` WHERE price > 1000 AND category = ' electronics ' ; Missing DROP Statement TEMP TABLEをDROPしないときに発生します。 CREATE TEMP TABLE `project.dataset.temp_table` (id INT64, name STRING); Dropped Persistent Table TEMP TABLEで事足りる際に、永続的なテーブルをCREATEして最後にDROPしているときに発生します。 CREATE TABLE `project.dataset.temp_table` (id INT64, name STRING); SELECT * FROM `project.dataset.temp_table`; DROP TABLE `project.dataset.temp_table`; 今回はこれに加えて自ら定義したアンチパターンを検知したいので、のちに追加します。 ミニマムな使い方 まず、ローカルでBigQueryのアンチパターン認識ツールを使う方法をご紹介します。 最初に、 BigQueryのアンチパターン認識ツールのリポジトリ をcloneします。このシステムはJavaで作られており、以下のコマンドでjibを使ってDockerコンテナイメージをビルドします。 mvn clean package jib:dockerBuild -DskipTests ビルドができたら以下でクエリの解析結果が返ってきます。 docker run \ -i bigquery-antipattern-recognition \ --query " SELECT * FROM \` project.dataset.table1 \` " また、特定のSQLファイルを解析するには以下のようなコマンドを入力します。 export INPUT_FOLDER = $( pwd ) /samples/queries/input export INPUT_FILE_NAME =multipleCTEs.sql docker run \ -v $INPUT_FOLDER : $INPUT_FOLDER \ -i bigquery-antipattern-recognition \ --input_file_path $INPUT_FOLDER / $INPUT_FILE_NAME 日本語がSQL内に含まれている際の問題 今回BigQueryのアンチパターン認識ツールを使用するにあたって、以下のようにSQLに日本語が入るとエラーが発生するという問題が発生しました。 --日本語ああああああああああああああああ SELECT title, language FROM `bigquery- public -data.samples.wikipedia` WHERE REGEXP_CONTAINS(title, ' .*aaaaa.* ' ) エラー内容は以下の通りです。 ERROR com.google.zetasql.toolkit.antipattern.util.AntiPatternHelper - index 138,length 138 java.lang.StringIndexOutOfBoundsException: index 138,length 138 at java.base/java.lang.String.checkIndex(String.java:3278) at java.base/java.lang.StringUTF16.checkIndex(StringUTF16.java:1470) at java.base/java.lang.StringUTF16.charAt(StringUTF16.java:1267) at java.base/java.lang.String.charAt(String.java:695) at com.google.zetasql.toolkit.antipattern.util.ZetaSQLStringParsingHelper.countLine(ZetaSQLStringParsingHelper.java:67) at com.google.zetasql.toolkit.antipattern.parser.visitors.IdentifyRegexpContainsVisitor.visit(IdentifyRegexpContainsVisitor.java:63) at com.google.zetasql.parser.ASTNodes$ASTFunctionCall.accept(ASTNodes.java:3592) at com.google.zetasql.parser.ParseTreeVisitor.descend(ParseTreeVisitor.java:45) (略) Javaの例外であるStringIndexOutOfBoundsExceptionが出ています。要約すると、文字列の指定されたインデックスが文字列長を超えているというエラーです。 このエラーの原因は、BigQueryのアンチパターン認識ツールがバイト長で文字列長をカウントしていることでした。アンチパターンが起こっている行を示すためにBigQueryのアンチパターン認識ツールは文字列長をカウントします。その手法こそがバイト長を文字列長として扱うというものでした。SQLがアルファベットや数字など1バイト文字だけで構成されている場合、バイト長と文字列長が一致するので問題ありません。しかし、SQLに日本語などのマルチバイト文字が入っている場合、バイト長>文字列長となってしまいエラーが発生します。 この解決手段として、バイト長を文字列長と一致させるようBigQueryのアンチパターン認識ツールのコードを改変しました。 改変内容は、BigQueryのアンチパターン認識ツールに PR を出しマージされたので、現在この問題は発生しません。 アンチパターンを定義する 今回は定義済みのアンチパターンに加えて不正なテーブル名をアンチパターンとして検出するという要件がありました。具体的には、以下のようにSQL内に出現するテーブル名の中にプロジェクト名が入っていない場合、プロジェクト名まで含めるよう促すというものです。 OK FROM `project_name.dataset_name.table_name` NG FROM `dataset_name.table_name` -- プロジェクト名が省略されている このように、自分で定義したアンチパターンを追加する方法をご紹介します。 前提として、BigQueryのアンチパターン認識ツールはSQLをAST(抽象構文木)に変換して、そのASTをトラバースして構文解析します。このときVisitorパターンを用います。したがって、新たなアンチパターンを定義するときには該当ノードをトラバースするVisitorを作成する必要があります。AntiPatternVisitorを実装する形でParseTreeVisitorを継承した新しいVisitorクラスを定義していきます。 // このソースコードは `src/main/java/com/google/zetasql/toolkit/antipattern/parser/visitors` フォルダに配置してください。既存のVisitorが置かれています。 public class IdentifyTableVisitor extends ParseTreeVisitor implements AntiPatternVisitor { public static final String NAME = "Table" ; private Set<String> tableNames = new HashSet<>(); private Set<String> withNames = new HashSet<>(); private ArrayList<String> result = new ArrayList<String>(); private final String SUGGESTION_MESSAGE = "テーブル名が不正です。プロジェクト名をテーブル名に追加してください %s." ; public IdentifyTableVisitor(String query) { this .query = query; } public void visit(ASTNodes.ASTTableExpression tableExpression) { if (tableExpression instanceof ASTNodes.ASTTablePathExpression) { visit((ASTTablePathExpression) tableExpression); } else if (tableExpression instanceof ASTNodes.ASTJoin) { visit(((ASTNodes.ASTJoin) tableExpression).getLhs()); visit(((ASTNodes.ASTJoin) tableExpression).getRhs()); } else if (tableExpression instanceof ASTNodes.ASTTableSubquery) { ASTNodes.ASTQueryExpression queryExpression = ((ASTNodes.ASTTableSubquery) tableExpression).getSubquery().getQueryExpr(); if (queryExpression instanceof ASTNodes.ASTSelect) { ASTNodes.ASTTableExpression tableExpression1 = ((ASTSelect) queryExpression).getFromClause().getTableExpression(); visit(tableExpression1); } } } @Override public void visit(ASTTablePathExpression tablePathExpression) { if (tablePathExpression.getPathExpr() != null ) { List<String> namePaths = tablePathExpression.getPathExpr().getNames().stream() .map(ASTIdentifier::getIdString).collect(Collectors.toList()); tableNames.addAll(namePaths); } if (tablePathExpression.getUnnestExpr() != null ) { String unNestExpressions = tablePathExpression.getUnnestExpr().getExpression().toString(); withNames.add(unNestExpressions); } } // ここでWITH句で定義されたテーブル名を抽出 @Override public void visit(ASTWithClause withClause) { List<ASTAliasedQuery> namePaths = withClause.getWith().stream().collect(Collectors.toList()); for (ASTAliasedQuery value : namePaths) { value.accept( this ); withNames.add(value.getAlias().getIdString()); } } private int countDot(String str) { int count = 0 ; for ( int i = 0 ; i < str.length(); i++) { if (str.charAt(i) == '.' ) { count++; } } return count; } public String getResult() { for (String tableName : tableNames) { int count = countDot(tableName); if (count != 2 && !withNames.stream().anyMatch(set -> set.contains(tableName))) { result.add(String.format(SUGGESTION_MESSAGE, tableName)); } } return result.stream().distinct().collect(Collectors.joining( " \n " )); } @Override public String getName() { return NAME; } } このコードで、テーブル名に当たるASTのノードを訪問し、ドットの数でテーブル名にプロジェクト名が含まれているかどうかを判断します。ドットが2個未満の場合はプロジェクト名が含まれていないという判定をします。しかし、WITH句で定義されたテーブル名に関してはこの限りではないので除外できるようにします。 そして以下のファイルのgetParserVisitorListに、定義したAntiPatternVisitorのインスタンスを追加します。 public List<AntiPatternVisitor> getParserVisitorList(String query) { return new ArrayList<>(Arrays.asList( new IdentifySimpleSelectStarVisitor(), new IdentifyInSubqueryWithoutAggVisitor(query), new IdentifyDynamicPredicateVisitor(query), new IdentifyOrderByWithoutLimitVisitor(query), new IdentifyRegexpContainsVisitor(query), new IdentifyCTEsEvalMultipleTimesVisitor(query), new IdentifyLatestRecordVisitor(query), new IdentifyWhereOrderVisitor(query), new IdentifyMissingDropStatementVisitor(query), new IdentifyDroppedPersistentTableVisitor(query), new IdentifyTableVisitor(query) // 追加 )); } これで不正なテーブル名をアンチパターンとして警告できるようになりました。 リンターとしてBigQueryのアンチパターン認識ツールを使用する際に生じる課題と解決策 BigQueryのアンチパターン認識ツールをそのまま使用する場合、Dockerコンテナを建てるかビルド済みのjarファイルをコマンドライン上で動作させます。使ってもらう際、各々の環境の違いもある中で動作環境を整え、解析対象のSQLを参照しコマンドを実行してもらう方法では手間がかかりすぎます。さらにエンジニア以外の使用も想定されるため、現実的ではありません。 そこで、Chromeの拡張機能として、BigQueryのコンソールで入力されたSQLをボタン1つで解析できるようにしました。SQLを投げると解析結果が返ってくるAPIを作成し、そのAPIをユーザが呼び出すという構成です。これによりコンソールにSQLを入力するだけで誰でも解析を掛けられるようになりました。 構成 作成したリンターを実際にChromeの拡張機能として使用する方法を紹介していきます。 APIサーバ化 BigQueryのアンチパターン認識ツールは、Spring Bootを使ってWebサービスとして使う環境が最初から整っています。それを利用してSQLをリクエストとして解析結果を返すAPIサーバを作成します。Spring Bootをセットアップする際のおおまかな手順は以下の通りです。 mainメソッドの変更 Spring BootのControllerを記述 出力メッセージのクラスを作成 まず、mainメソッドでSpring Bootを起動させられるようにします。 @SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main. class , args); } } 続いて、実際に構文解析するコードをSpring BootのControllerとして書き直します。基本的に改変する前のmainメソッドと同様ですが、1つのSQLを受け取り1つの解析結果を返せるようにします。 public class QueryRequest { private String query; public String getQuery() { return query; } } @RestController public class MainController { private static int countQueriesWithAntipattern = 0 ; @PostMapping ( "/" ) public Map<String, Object> processQuery( @RequestBody QueryRequest queryRequest) { try { String query = queryRequest.getQuery(); String replies[] = new String[ 1 ]; AntiPatternCommandParser cmdParser = new AntiPatternCommandParser( new String[] {}); AntiPatternHelper antiPatternHelper = new AntiPatternHelper( cmdParser.getProcessingProject(), cmdParser.useAnalyzer()); OutputWriterForResponse outputWriter = new LogOutputWriterForResponse(); Boolean rewriteSQL = cmdParser.rewriteSQL(); outputWriter.setRewriteSQL(rewriteSQL); InputQuery inputQuery = new InputQuery(query, "query provided by param:" ); StringBuilder result = executeAntiPatternsInQuery(inputQuery, outputWriter, cmdParser, antiPatternHelper); result.append(logResultStats()); outputWriter.close(); replies[ 0 ] = result.toString(); return Map.of( "replies" , replies); } catch (Exception e) { return Map.of( "errorMessage" , e.toString()); } } private StringBuilder executeAntiPatternsInQuery(InputQuery inputQuery, OutputWriterForResponse outputWriter, AntiPatternCommandParser cmdParser, AntiPatternHelper antiPatternHelper) { StringBuilder stringBuilder = new StringBuilder(); try { List<AntiPatternVisitor> visitorsThatFoundAntiPatterns = new ArrayList<>(); // parser visitors antiPatternHelper.checkForAntiPatternsInQueryWithParserVisitors(inputQuery, visitorsThatFoundAntiPatterns); // analyzer visitor if (antiPatternHelper.getUseAnalizer()) { antiPatternHelper.checkForAntiPatternsInQueryWithAnalyzerVisitors(inputQuery, visitorsThatFoundAntiPatterns); } // rewrite if (cmdParser.rewriteSQL()) { GeminiRewriter.rewriteSQL(inputQuery, visitorsThatFoundAntiPatterns, antiPatternHelper, cmdParser.getLlmRetriesSQL(), cmdParser.getLlmStrictValidation()); } // write output if (!visitorsThatFoundAntiPatterns.isEmpty()) { return outputWriter.writeRecForQuery(inputQuery, visitorsThatFoundAntiPatterns, cmdParser); } return stringBuilder; } catch (Exception e) { System.out.println(e); return stringBuilder; } } private static String logResultStats() { StringBuilder statsString = new StringBuilder(); statsString.append( " \n * Queries with anti patterns: " + countQueriesWithAntipattern); return statsString.toString(); } } そして、レスポンスで使う出力メッセージ作成クラスを追加します。 // このソースコードは `src/main/java/com/google/zetasql/toolkit/antipattern/output` フォルダに配置してください。 public abstract class OutputWriterForResponse { private boolean rewriteSQL = false ; public abstract StringBuilder writeRecForQuery(InputQuery inputQuery, List<AntiPatternVisitor> visitorsThatFoundPatterns, AntiPatternCommandParser cmdParser) throws IOException; public void close() throws IOException {}; public void setRewriteSQL( boolean rewriteSQL) { this .rewriteSQL = rewriteSQL; } } // このソースコードは `src/main/java/com/google/zetasql/toolkit/antipattern/output` フォルダに配置してください。 public class LogOutputWriterForResponse extends OutputWriterForResponse { private static final Logger logger = LoggerFactory.getLogger(LogOutputWriter. class ); public StringBuilder writeRecForQuery(InputQuery inputQuery, List<AntiPatternVisitor> visitorsThatFoundPatterns, AntiPatternCommandParser cmdParser) { StringBuilder outputStrBuilder = new StringBuilder(); outputStrBuilder.append( " \n " + "-" .repeat( 50 )); outputStrBuilder.append( " \n Recommendations for query: " + inputQuery.getQueryId()); for (AntiPatternVisitor visitor: visitorsThatFoundPatterns) { outputStrBuilder.append( " \n * " + visitor.getName() + ": " + visitor.getResult()); } if (cmdParser.rewriteSQL() && inputQuery.getOptimizedQuery() != null ) { outputStrBuilder.append( " \n * Optimized query: \n " ); outputStrBuilder.append(inputQuery.getOptimizedQuery()); } outputStrBuilder.append( " \n " + "-" .repeat( 50 )); outputStrBuilder.append( " \n\n " ); return outputStrBuilder; } } これで構文解析の機能をAPIリクエストでSQLを投げることで使用できるようになりました。 次に作成したAPIサーバをデプロイします。Cloud Runにデプロイするまでの流れは以下の通りです。 Artifact Registryにリポジトリを作成 Docker imageをビルド、タグ付け Docker imageのpush Cloud Runにデプロイ まず、Artifact Registryにリポジトリを作成します。形式はDockerを選択します。 次に、実際にpushします。 以下でDocker imageをビルドします。 mvn clean package jib:dockerBuild -DskipTests Docker imageにタグ付けします。 docker tag bigquery-antipattern-recognition [ REGION ] -docker.pkg.dev/ [ PROJECT_ID ] / [ REPOSITORY_NAME ] / [ IMAGE ] そして、pushします。 docker push [ REGION ] -docker.pkg.dev/ [ PROJECT_ID ] / [ REPOSITORY_NAME ] / [ IMAGE ] 次にイメージをCloud Runにデプロイします。 gcloud run deploy --image [ REGION ] -docker.pkg.dev/ [ PROJECT_ID ] / [ REPOSITORY_NAME ] / [ IMAGE ] : [ TAG ] --platform = managed --project =[ PROJECT_ID ] これでCloud Run上に、POSTリクエストでSQLをbodyに含めれば解析結果が返ってくるAPIサーバをデプロイできました。次に社員のみがこのAPIを使用できるようにするため、IAPによる認証をつけます。IAPはロードバランサ上で動作するので、まずロードバランサを作成します。そしてCloud Runサービスをバックエンドサービスとしてロードバランサに紐づける作業を先にします。具体的な流れは以下の通りです。 外部静的アドレスを取得 DNSの設定 ロードバランサを作成 作成したCloud Runサービスをロードバランサに紐づける ロードバランサのフロントエンドの設定 IAPの設定 まず、ロードバランサに接続するための外部静的アドレスを「VPCネットワーク - IPアドレス」から予約します。 次にCloud DNSからレコードセットを作成します。リソースレコードのタイプはAとし、IPv4アドレスには予約したIPアドレスを紐づけます。IPアドレスの設定ができたらロードバランサを作成します。 「ロードバランサの作成」を選択し、外部のアプリケーションロードバランサを作成します。 次にバックエンドの構成を設定します。「バックエンドサービスとバックエンドバケット」からバックエンドサービスを作成します。バックエンドタイプを「サーバーレスネットワークエンドポイントグループ」に設定し、新しいバックエンドとして先ほど作成したCloud Runサービスを指定します。 これでロードバランサとCloud Runサービスが紐づきました。 フロントエンドの構成では、プロトコルをHTTPSにします。IPアドレスに先ほど設定したものを指定します。証明書は、「新しい証明書を作成」で先ほど作成したドメインを指定し、Googleマネージドの証明書を作成します。 ロードバランサを作成できました。次に、このロードバランサに対してIAPで認証をかけます。Identity-Aware Proxyのコンソール画面から設定します。 バックエンドサービスの中からIAPの認証をかけたいものを選び、IAPのトグルをオンにします。そして、アクセス権の設定をします。プリンシパルを追加し、「IAP-secured Web App User」ロールを割り当てます。割り当てられたプリンシパルは今回作成したAPIサーバへアクセスできるようになります。 最後に当該バックエンドサービスの設定画面で、最下部の「HTTPオプションを有効にする」にチェックを入れておきます。こちらについては後述します。 Chrome拡張 次に、クライアントサイドの作成方法を紹介します。Chrome拡張を用いてBigQueryのコンソール画面に解析ボタンを設置しました。ユーザが解析ボタンを押すと、エディタに入力したSQLをリクエストボディとして先ほど作成した解析用のAPIリクエストを送信できるようにしました。解析結果はモーダルで表示します。 Chrome拡張を作成する際、以下のようなディレクトリ構造になります。 linter-extension ├── content.js ├── manifest.json └── styles.css manifest.jsonは、拡張の構成や権限、動作方法を定義します。host_permissionsにAPIサーバのURLを記述、content_scriptsのmatchesにDOMを操作するサイトのURLを記述しておきます。 { " manifest_version ": 3 , " name ": " linter ", " version ": " 1.0 ", " permissions ": [ " cookies ", " activeTab " , ] , " action ": { " default_popup ": " popup.html " } , " content_scripts ": [ { " matches ": [ " https://console.cloud.google.com/bigquery* " ] , " js ": [ " content.js " ] , " css ": [ " styles.css " ] } ] , " host_permissions ": [ " https://[作成したAPIサーバのドメイン]/* " ] } content.jsには、実際にページのDOMを操作して要素を追加、削除、変更するJavaScriptを書きます。今回の場合は以下のような内容を書きます。 コンソール画面へのボタンの追加 エディタに書かれたSQLの読み取り APIをリクエストする 初回の認証を通すコードは IAP セッションの管理 を参照しました。 // エディタからSQLを取得するコード function getCombinedText () { const formParent = document . querySelector ( '[エディタのセレクタ]' ) ; if ( formParent ) { console . log ( formParent ) const content = formParent . innerText || formParent . textContent ; return content . replace (/ [\r\n \ \ ] + / g , '' ) ; } else { return "" ; } } // 初回の認証を通すコード var iapSessionRefreshWindow = null ; function sessionRefreshClicked () { if ( iapSessionRefreshWindow == null ) { iapSessionRefreshWindow = window . open ( "/?gcp-iap-mode=DO_SESSION_REFRESH" ) ; window . setTimeout ( checkSessionRefresh , 500 ) ; } return false ; } function checkSessionRefresh () { if ( iapSessionRefreshWindow ! = null && ! iapSessionRefreshWindow . closed ) { fetch ( "/" , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , credentials : 'include' , }) . then ( function ( response ) { if ( response . status === 401 ) { window . setTimeout ( checkSessionRefresh , 500 ) ; } else { iapSessionRefreshWindow . close () ; iapSessionRefreshWindow = null ; } }) ; } else { iapSessionRefreshWindow = null ; } } // APIを叩くボタンを設置する function addButton () { const linterUrl = '[APIサーバのURL]' ; const modalHTML = ` <div id="modal" class="modal"> <div class="modal-content"> <span class="close">×</span> <div id="modalText"></div> </div> </div> ` ; document . body . insertAdjacentHTML ( 'beforeend' , modalHTML ) ; const modal = document . getElementById ( "modal" ) ; const modalText = document . getElementById ( "modalText" ) ; const close = document . getElementsByClassName ( "close" )[ 0 ] ; function openModal ( message ) { modalText . textContent = message ; modal . style . display = "block" ; } close . onclick = function () { modal . style . display = "none" ; } window . onclick = function ( event ) { if ( event . target === modal ) { modal . style . display = "none" ; } } function addButtonToActionBars () { const parentDiv = document . querySelectorAll ( "[ボタンを追加したい親要素]" ) ; const newDiv = document . createElement ( 'div' ) ; const button = document . createElement ( 'button' ) ; newDiv . appendChild ( button ) ; parentDiv . appendChild ( newDiv ) ; button . addEventListener ( 'click' , async () => { const query = getCombinedText () ; try { const response = await fetch ( linterUrl , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , body : JSON . stringify ({ "query" : query }) , credentials : 'include' , }) ; if ( response . status === 401 ) { button . onclick = sessionRefreshClicked () ; } else if ( ! response . ok ) { console . log ( response ) ; throw new Error ( `HTTP error! Status: ${ response . status } ` ) ; } else { const data = await response . json () ; openModal ( data . replies . join ( '\n' )) ; } } catch ( err ) { openModal ( err ) ; } }) ; } ; addButtonToActionBars () ; } window .onload = addButton ; 今回はBigQueryのコンソールからAPIサーバにクロスオリジンでリクエストを送っています。 かつ、Content-Typeヘッダにapplication/jsonを指定してPOSTリクエストをしているため、リクエストの前にプリフライトリクエストが発生します。先述した「HTTPオプションを有効にする」をチェックしない場合プリフライトリクエストが正常にサーバ側に届かないので注意してください。 動作例 最後に作成したChrome拡張を有効化します。Chromeでchrome://extensions/にアクセスして拡張機能ページを開き、画面右上のデベロッパーモードをオンにします。 そして画面左上の「パッケージ化されていない拡張機能を読み込む」をクリックします。すると拡張機能のディレクトリを選択できるようになるので、作成した拡張機能のディレクトリを選択します。すべての拡張機能の欄に作成したものが追加されたことを確認してください。 拡張機能を有効にすると以下のような解析ボタンが現れます。 クエリを入力し、解析ボタンを押すと解析されます。 まとめ 今回はChrome拡張としてBigQueryのアンチパターン認識ツールを利用して独自のSQLリンターを作成できました。ぜひ参考にしていただけると幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、XR × Fashion TechやXR × Beauty Techといった領域を推進している創造開発ブロックの @ikkou です。 2025年1月7日から10日の4日間にかけてラスベガスで開催された「 CES 2025 」に一般参加者として現地参加してきました。なぜZOZOがCESに参加するのか、疑問に思われる方もいるでしょう。私自身が注力しているXR領域に関する最新動向の調査と、ZOZOとしても親和性の高いFashion Tech・Beauty Techのトレンドを一次情報として得るためです。現地で最新技術を直接体験することで、記事やニュースでは得られない深いインサイトを得られます。 私個人としては通算で6度目、ZOZO所属としては2020年、2023年、2024年に続き4度目の参加となります。継続して参加することで定点観測の意味もあります。 CES 2024 参加レポート - コロナ禍以前の活況を取り戻した CES CES 2023 参加レポート - 3年ぶりの現地参加 CES 2020参加レポート: 現地参加3年目の目線で視た #CES2020 前半はCESの概要と関連する情報のアップデートを、後半は特に私が注目したトピックについてお伝えします。 CES 2025全体のトレンドについては、Day 2から会場で配布されているCES Daily Showのデジタル版 Day 1 ・ Day 2 ・ Day 3 などをご覧ください。 CESとは CES 2025の6つのトレンドと49個のカテゴリー CES 2025のメインテーマは「DIVE IN」 出展社数と参加者数の推移 チケットの価格 会場の概要 会場間の移動 Vegas Loop Official CES Store XR Tech XR関連企業の出展動向 CES Innovation Awardsから見るXRデバイスのトレンド 出展ブースのXRデバイスから見るXRデバイスのトレンド ARグラスとスマートグラス 補助デバイスとしてのスマートグラス エルシオによる「オートフォーカスグラス」 EssilorLuxotticaによる「Nuance Audio」 Gentex Corporationによる「eSight Go」 要素技術 Cellidによる「最新のウェイブガイド」 DUAL MOVEによる「tXR display」 AGCによる「AR/MRグラス向け高屈折率ガラス基板」 Fashion TechとBeauty Tech コーセーによる「Mixed Reality Makeup」 アシックスとダッソー・システムズによる「パーソナライズドフットウェア」 Prinkerによる「Prinker POP」 その他の気になったFashion TechとBeauty Tech ロレアルグループによる「ロレアル セル バイオプリント」 Withingsによるスマートミラー「OMNIA」 おわりに CESとは Venetian Expoの1Fと2Fをつなぐエスカレーター付近に設置されているCESの看板 CES はCTA(Consumer Technology Association)が主催する、毎年1月にラスベガスで開催される世界最大級を誇る「 テクノロジーのショーケース 」です。 読み方は「せす」と呼ぶ方もいますが、正しくは「 シーイーエス 」です。CESはかつて『Consumer Electronics Show』と呼ばれていましたが、現在この表記は使用されていません。CESは現在、何かの略称ではなく、単独の名称として使用されています。 CES 2025からCESのロゴが刷新された 昨年、CESを主催しているCTAは記念すべき100周年を迎えました。そしてCES 2025にあわせてCTAとCESのロゴがリニューアルされました。 CES 2025の6つのトレンドと49個のカテゴリー CESには毎年CTAの打ち出すトレンドがあり、 2025年1月5日に発表されたCTAのプレスリリース によると、2025年のトップトレンドとして、以下の6つが発表されました。 AI / Artificial Intelligence デジタルヘルス / Digital Health エネルギートランジション / Energy Transition モビリティ / Mobility クオンタム / Quantum サステナビリティ / Sustainability 2024年はAI・万人のための人間の安全保障・モビリティ・サステナビリティの4つでした。2025年は万人のための人間の安全保障の代わりに、デジタルヘルス・エネルギートランジション・クオンタムが加わった形になります。 トップトレンドとは別に、次に挙げる49の技術領域が公式カテゴリーとして設定されています。 3D Printing, 5G Technologies, Accessibility, Accessories, AgTech, AR/VR/XR , Artificial Intelligence, Audio, Beauty Tech , Blockchain, Cloud Computing, Construction Tech, Content and EntertaInment, Cryptocurrency, Cybersecurity, Digital Health, Drones, Education Tech, Energy Transition, Energy/Power, Enterprise, Fashion Tech , Fintech, Fitness, Food Tech, Gaming and Esports , Home Entertainment and Office Hardware, Imaging, Investing, IoT/Sensors, Lifestyle, Marketing and Advertising, Metaverse, NFT, Quantum Computing, Retail/E-Commerce , Robotics, Smart Cities, Smart Home and Appliances, Sourcing and Manufacturing, Space Tech, Sports, Startups, Streaming, Supply and Logistics, Sustainability, Travel and Tourism, Vehicle Tech and Advanced Mobility, Video かつて『家電見本市』と形容されていたCESですが、現在は家電だけに留まらず、非常に幅広い技術領域を網羅していることが印象的です。私が注目しているAR/VR/XRをはじめとする技術領域は太字で示しています。 CES 2025のメインテーマは「DIVE IN」 LVCC West Hallに設置されている#CES 2025のモニュメント CES 2024のメインテーマ「All ON」に続くCES 2025のメインテーマは「DIVE IN」でした。直訳すると「飛び込む」ですが、物事に対して積極的に取り組むという意味もあります。未来への飛躍を象徴するテーマと言えるでしょう。 出展社数と参加者数の推移 コロナ禍以降の出展社数は減少しましたが、CES 2025では過去最高の4,500社以上が出展していました。参加者数は最盛期ほどではありませんが、昨年・一昨年に比べると少しずつ戻ってきています。コロナ禍前後の出展社数と参加者数の推移は以下の通りです。 年度 出展社数 参加者数 CES 2018 3,900社以上 182,198人 CES 2019 4,500社以上 175,212人 CES 2020 約4,500社 171,268人 CES 2021 約2,000社 約80,000人 CES 2022 2,300社以上 44,000人以上 CES 2023 3,200社以上 117,841人 CES 2024 4,300社以上 138,789人 CES 2025 4,500社以上 141,000人以上 なお、CESの参加者数は1桁単位で精密な数字が公表されていますが、これは参加バッジの発行数をもとにした重複カウントなしの純粋な参加者数です。CES 2025の正確な参加者数は後日公開される見込みです。 チケットの価格 LVCC West Hallのエントランスを仰ぐ CES 2025の参加登録は2025年9月11日に開始され、チケットの価格は同12月5日までの早期登録が149 USD、以降は350 USDでした。私は昨年に続き「貴重なCESの卒業生(英語表記は“valued CES alum”)」向けの特典により無料でした。 直近半年間でドル円の為替相場が大きく変動したため、チケットの取得タイミングによっては多少の差が生じました。例えば早期登録の期間中にチケットを購入していた場合は149 USDで約21,200〜22,400円の変動幅に、通常登録の期間中にチケットを購入していた場合は350 USDで約52,500〜55,200円の変動幅になります。 昨年はCES公式のカンファレンスプログラミングパスを追加しましたが、今年は追加せずにブースのみを巡りました。これは単純に、カンファレンスに参加するとブースを巡るための時間を削減せざるを得ないこともありますが、 セッション動画は後日公開される ため、そこに金銭的・時間的コストをかける必要はないと判断しました。これは参加する目的によっても異なると思いますが、私にとっては、ブースを巡ることが主目的のため、今回はカンファレンスプログラミングパスを追加しませんでした。 会場の概要 Image Source. https://www.ces.tech/explore-ces/maps-and-locations/ CESの展示会場はこれまでTech East・Tech West・Tech Southという3つのエリアに大別されていましたが、CES 2025ではTech Eastが LVCC Campus に、Tech Westが Venetian Campus に、そしてTech Southが C Space Campus にリネームされました。 工事が完了し再び利用されるようになったLVCC South CES 2024ではTech Eastに Westgate と Renaissance が含まれていましたが、CES 2025ではLVCCのみになりました。また、工事の完了したLVCC Southが再び利用されるようになりました。結果的にLVCCのみとなったことがリネームの理由と考えられます。同様にTech WestもVenetian Expoに統合されました。Tech SouthはC Spaceのため会場だったため、よりわかりやすい会場名になったと言えるでしょう。 一般の来場者向けの会期は4日間ありますが、1人で全ての会場・全てのブースを巡るのは非現実的です。私は例年通り主にマーケター向けのC Space Campusには行かず、以下の工程で会場を巡りました。 Day 1:LVCC Campus(WestとNorth) Day 2:Venetian Campus全体 Day 3:LVCC Campus(Central) Day 4:LVCC Campus(South) 4日間とも、とにかく歩き回りました。歩きやすい履き慣れた靴の準備が欠かせません。事前にある程度は計画して効率的に巡っているとはいえ、4日間の平均歩数は1日平均20,000歩を超えていました。 会場間の移動 とにかく会場間の移動には時間がかかります。今回、会場間の移動には徒歩、Vegas Loop、そしてLyftを利用しました。徒歩とLyftについては昨年からの特筆すべきアップデートはありませんでした。 Vegas Loop Vegas Loopで見かけたTesla Model X 昨年、一昨年と毎回お世話になっている Vegas Loop ですが、今年もLVCC各ホール間の移動に何度か利用しました。 Image source. https://www.lvcva.com/vegas-loop/ Vegas LoopはLVCCのWest Hall・Central Hall・South Hall間を結ぶ無償のLVCCラインと、RESORT WORLDとLVCCの間を結ぶ有料のRESORT WORLDラインがあります。RESORT WORLDラインは開通当初、1日乗り放題で2.5 USDでしたが、CES 2023時は1日乗り放題で4.5 USDに値上がりしました。そしてCES 2024では5 USDに、今年のCES 2025では10 USDと大幅に値上がりしていました。 WESTGATE RESORT STATIONが延伸される予定 各ステーションに掲示された路線図には、LVCC RIVIERA STATIONからWESTGATE RESORTへの延伸予定が示されていました。これは WESTGATE RESORTのページでも示されています 。 The new Vegas Loop will connect to the existing station, making it easy for guests of Westgate Resorts to get around town. そして、この路線は1月18日に開通したことが、 The Boring CompanyのXアカウントで投稿されています 。 CES 2025ではWESTGATEが会場として利用されることはありませんでしたが、過去には利用されていたので、再び会場として利用される可能性も考えられます。また、会期中はビジネスミーティングのためのプライベートブースとして利用されているので、この延伸はビジネスミーティングの利便性向上にも寄与するでしょう。 Official CES Store Official CES Store @ LVCC North Lobby CES 2025ではCES公式のOfficial CES Storeが会場内に4ヵ所設置されていました。名称は異なりますが、CES 2024ではOfficial Show Storeとして同様のストアが会場内に5箇所設置されていました。 Official CES Storeで販売されていた$40の公式Tシャツ 様々なグッズが販売されていましたが、昨年同様、Tシャツやフーディーは生成AIで作成されたと思われるビジュアルが多く感じました。特にモナリザがVR HMDを装着しているユニークなデザインに惹かれました。 XR Tech 前述の通り、CESのカテゴリーのひとつに「 AR/VR/XR 」が設けられています。このパートでは、それらをXR Techとしてまとめてお伝えします。 XR関連企業の出展動向 Image source. https://exhibitors.ces.tech/8_0/floorplan/?hallID=B CES 2025では、LVCC Central Hallに「 GAMING | XR 」とカテゴライズされた一画が設けられていました。「 AR/VR/XR 」と「 Gaming and Esports 」が統合された形です。 CES 2020では「 AR/VR & Gaming 」、CES 2023、2024では「 GAMING | METAVERSE | XR 」でした。私は昨年のレポート記事で「 個人の感覚として、2024年においてもメタバースムーブメントが去年同様盛り上がっているかどうかについて疑義があります 」と述べました。その通り、いよいよ「メタバース」というキーワードがカテゴリー名から消えました。事実、メタバースを全面に押し出したブースはほとんど見られませんでした。 Image source. https://exhibitors.ces.tech/8_0/floorplan/?hallID=B このLVCC Centralの「GAMING | XR」エリアには62ブースが出展していました。VR HMDの「Pimax」、アクションカメラの「Insta360」、そしてARグラスの「XREAL」など中国企業が目立っていました。 LVCC Central Hallに単独初出展した日本発XR企業のShiftall(左)とDiver-X(右) 一方で、これまでPanasonicのブース内に出展していた「Shiftall」は独立してブースを初出展しました。また、CES 2023でJ-Startup枠に出展していた「Diver-X」も単独でブースを初出展するといった日本発XR企業が躍進している様子が見受けられました。 また、例年はスタートアップ企業が集まるVenetian CampusのEureka ParkにもXR関連エリアが設けられていましたが、CES 2025では見当たりませんでした。 CES 2023では「Gaming/Metaverse/XR」エリアとして16社が出展、CES 2024では「Gaming/XR」エリアとして7社が出展していました。このように、近年では関連するブース数が減少傾向にありましたが、CES 2025では、いよいよエリア自体が消失してしまいました。 ただし、XR関連ブースが必ずしもこれらのエリアに出展しているとは限りません。LVCC Campus・Venetian Campusともに、このエリアに限らずXR関連のブースは多くあり、 「AR/VR/XR」カテゴリーとして登録されているブース 数は355にも及びました。 CES Innovation Awardsから見るXRデバイスのトレンド CESでは毎年、デザインやエンジニアリングにおいて優れた製品を表彰する CES Innovation Awards を実施しています。XR関連の製品は XR Technologies & Accessories としてカテゴライズされ、いわゆるHMD型・眼鏡型のXRデバイスは次の4製品が受賞しました。 Rokid Cupcake AR Glasses Sony XR Head-Mounted Display SRH-S1 Ultra Lightweight Polychromatic AR+AI Glasses XREAL One and XREAL One Pro 3/4が眼鏡型のXRデバイスでした。これは、HMD型のXRデバイスが一般的になりつつある中で、眼鏡型のXRデバイスが注目されていることを示していると言えるでしょう。 ただし、CES Innovation Awardsは自発的な応募が必要なため、すべてのXRデバイスの中から選出されているわけではありません。そこで、次に出展ブースで実際に試したXRデバイスからトレンドを見ていきます。 出展ブースのXRデバイスから見るXRデバイスのトレンド CES 2025で体験したXRデバイス(HMD型・眼鏡型)の一部 例年通り、CES 2025でもたくさんのXRデバイスを試してきました。この写真はHMD型・眼鏡型のXRデバイスの一部です。この他にも、例えば「 Mudra Band 」のような手首に装着する筋電デバイスもあります。 写真から一目瞭然ですが、非常に多くの眼鏡型デバイスが出展されていました。これは、HMD型デバイスが普及する中、眼鏡型デバイスへの関心が高まっていることを示していると考えています。 ARグラスとスマートグラス 一口に眼鏡型デバイスといっても、その機能や性質から「ARグラス」と「スマートグラス」に大別できます。 ARグラスは、現実世界にデジタル情報を重ね合わせることができるデバイスです。CES 2025への出展はありませんでしたが、SnapchatのSnapがクリエイター向けに展開している「 Spectacles 」は代表的なARグラスです。ARグラスは原則としてディスプレイを搭載し、スマートグラスと比較してトラッキング性能が優れていることが特徴です。 ARグラスに対してAR機能を持たない眼鏡型デバイスがスマートグラスです。市販されているMetaとRay-Banによる「 Ray-Ban Meta 」やAmazonの「 echo frames 」、そしてCES 2025の会場で試したほとんどの眼鏡型デバイスはスマートグラスです。スマートグラスはディスプレイを搭載しているものと搭載していないものに細分化できます。CES 2025では特にAI機能を搭載し、リアルタイムで会話を翻訳するスマートグラスを多数見かけました。 ここでスマートグラスの例として挙げている「Ray-Ban Meta」はまだ日本で販売されていませんが、USではある程度普及しているように感じました。ラスベガス市内で「Ray-Ban Meta」を取り扱っている店舗で聞いた話では、継続的に品薄または売り切れ状態が続いているとのことでした。 補助デバイスとしてのスマートグラス XR Techというと五感の内、視覚に焦点が当てられがちですが、これは「見える」ことを前提としています。しかし、加齢や健康状態の影響で視覚を含む五感の機能が低下することもあります。CESでは、こうした視覚や聴覚の補助を目的としたデバイスもいくつか出展されていました。 エルシオによる「オートフォーカスグラス」 大阪大学発スタートアップのエルシオによる「オートフォーカスグラス」 出典: https://elcyo.com/ 大阪大学発スタートアップのエルシオは、自動でピントが合うメガネ「オートフォーカスグラス」を出展していました。見る距離や目の状態に合わせてフレネル液晶レンズが変化し、近視・遠視に問わずひとつの眼鏡で快適な視界を提供するものです。ジャンルとしてはスマートグラスに分類されますが、視覚の補助デバイスの一例と言えます。 技術的なアプローチは異なりますが、CES 2024に出展していたViXionも「 ViXion01S 」というオートフォーカスアイウェアを2025年5月に発売予定です。 EssilorLuxotticaによる「Nuance Audio」 EssilorLuxotticaによる「Nuance Audio」 出典: https://www.nuanceaudio.com/en-us/c/hearing-glasses EssilorLuxottica はCES 2024に続き、CES 2025でも聴覚を補助する機能を持つヒアリンググラス「Nuance Audio」を出展していました。眼鏡型デバイスなので、視覚にあわせて聴覚も補助できるヒアラブルデバイスとも言えます。 EssilorLuxotticaは Ray-Ban Metaも手掛ける 世界最大手の眼鏡ブランドで、和真眼鏡の和真や福井めがね工業など、複数の日本企業も傘下に持っています。 USでは補聴器に分類されるため、現在はFDAの承認待ちとのことです。2024年にはAppleのワイヤレスイヤホン「AirPods Pro 2」も補聴器としてFDAが承認し、日本でも同年11月から ヒアリング補助プログラムが提供されています 。 私は以前患った突発性難聴が完治せず、聴覚に対して課題を抱えているため、このようなヒアラブルデバイスに対しては特に興味を持っています。普段使いしやすい見た目にも惹かれます。 Gentex Corporationによる「eSight Go」 Gentex Corporationによる「eSight」 出典: https://www.nuanceaudio.com/en-us/c/hearing-glasses 「 eSight Go 」は加齢に伴う黄斑変性などを始めとする課題を、カメラを通した映像で自然に補える視覚障がい者向けのウェアラブル補助デバイスです。ズーム機能により、遠方の対象を拡大して見ることもできます。 Gentex Corporationは例年CESでeSightを含む各種製品を出展しています。もともとはeSightと協力して開発していたデバイスですが、2024年の買収により、Gentex Corporationによって買収されています。 要素技術 CESでは完成している製品の他、製品を構成する要素技術となるものも多数出展されていました。その中で日本発の気になるものをいくつか紹介します。 Cellidによる「最新のウェイブガイド」 CellidによるメガネタイプARグラスと最新のウェイブガイド 出典: https://cellid.com/news/20241216 世界最大級の視野角を持つガラス製ウェイブガイドをはじめとするARグラス用ディスプレイを開発するCellidは、 メガネタイプARグラスのリファレンスデザイン の他、 最新のウェイブガイド を出展していました。 CellidはCES 2022以降、毎年出展している日本発のスタートアップで、これまではウェイブガイドを中心に出展していましたが、いよいよ自社開発のARグラスを開発しました。毎年確実に進化を遂げていて、将来的には数多の眼鏡型デバイスに使用されると考えています。 DUAL MOVEによる「tXR display」 JAPAN TECH 出展企業のひとつであるDUAL MOVEは、透過型の裸眼立体視ディスプレイ「 tXR display 」を出展していました。 これは、眼鏡型デバイス向けのものではなく、自動車のフロントガラスなどに搭載しているもので、いわゆる「車窓XR」として利用されることを想定しています。一般的なディスプレイと比較すると解像度は高くありませんが、透過で利用できることと、裸眼で立体視可能であることが大きな特徴です。 CESではモビリティも人気の技術領域で、特に自動運転は注目されています。tXR displayのような透過型の裸眼立体視ディスプレイはナビゲーション用途だけではなく、自動運転中のエンターテイメントにも活用できると考えられています。日本でも複数の「XRバス」が既に運用されていますが、その多くは平面なので、立体視ディスプレイを搭載することで、よりリアルな体験が可能になるかもしれません。 AGCによる「AR/MRグラス向け高屈折率ガラス基板」 ガラスをはじめとする製品を取り扱うAGCは、AR/MRグラス向けの 高屈折率ガラス基板 「 M100/200シリーズ 」を出展していました。これは眼鏡型デバイス向けのディスプレイに使用することを想定したもので、視野角の拡大や画像の鮮明化に繋がるとされています。 Fashion TechとBeauty Tech CES 2024でBeauty Techの集まる一画が設けられたものの、カテゴリーとしては存在していませんでした。しかし、CES 2025では注目の高まりに伴い、 Fashion TechとBeauty Techがそれぞれカテゴリーとして設けられました 。また、CES Innovation Awardsのカテゴリーとしても設けられています。 「Fashion Tech」カテゴリーとして登録されているブース 数は97、そして 「Beauty Tech」カテゴリーとして登録されているブース 数は130でした。ただし、カテゴリーは出展企業が複数指定できますが、これまではDigital Healthカテゴリーなどを設定していた企業も多く、必ずしもFashion TechやBeauty Techとして登録されているわけではありませんでした。個人的に興味を惹かれたブースをいくつか紹介します。 コーセーによる「Mixed Reality Makeup」 コーセーによる「Mixed Reality Makeup」 CES 2023に初出展したコーセーが、当時出展した『デジタルパーソナルカラー体験「COLOR MACHINE」』を『 Mixed Reality Makeup 0 min try-on studio 』として進化させて出展していました。先に紹介したCES Innovation AwardsのXR Technologies & Accessories部門にも選出されています。 これはいわゆる「ARメイク」や「Virtual Try-on」と呼ばれる取り組みで、プロジェクターで顔にメイクのテクスチャを投影することで、実際にメイクをしているかのような体験ができます。化粧品の色味を正確に再現することと、1000fpsの超高速プロジェクションマッピングにより顔の動きに追随することが特徴です。 体験している様子を私のXアカウントに投稿している ので、興味がある方はぜひご笑覧ください。 Mixed Reality Makeupの体験後、実際にタッチアップしてもらえた この取り組みは実店舗に設置することを想定しています。この記事を読んでいる皆さんも コーセーの旗艦店である「Maison KOSE銀座店」で「COLOR MACHINE」を体験できます 。 私たちも「 ZOZOCOSME 」の「 ARメイク 」や「 WEAR by ZOZO 」の「 WEARお試しメイク 」を提供していますが、オンラインでのEコマースとは異なる、オフラインの実店舗ならではの取り組みとして注目しています。 アシックスとダッソー・システムズによる「パーソナライズドフットウェア」 アシックスとダッソー・システムズによる「パーソナライズドフットウェア」 日本発のアシックスは、2024年7月にフランス発のダッソー・システムズと提携し、パーソナライズドフットウェアを開発すると 発表しました 。ダッソー・システムズのブースではこの取り組みの一環として、既に発売されている3Dプリント製のサンダル「 ACTIBREEZE 3D SANDAL 」の他、スニーカーのプロトタイプも展示されていました。このスニーカーを試し履きしましたが、中空構造の独特な歩き心地がとても印象的でした。 私たちも計測技術のひとつとして足の形をミリメートル単位で3D計測できる「 ZOZOSHOES 」を提供しているので、ある側面では競合にあたるかもしれません。しかし、こうした取り組みは、計測技術を用いたパーソナライズドフットウェアの普及に向けた一歩と言えるでしょう。 ブースの様子は ダッソーシステムズのFacebookページで確認できます 。映像中にも登場しますが、Meta Quest 3を用いたXRコンテンツも体験できました。 Prinkerによる「Prinker POP」 Prinkerの新製品「Prinker POP」 例年CESにテンポラリータトゥーデバイスを出展していたPrinkerは、CES 2025にあわせて「 Prinker POP 」を発表、出展しました。Prinker POPは、従来のPrinkerとは異なり、キオスク端末でユーザーがカスタムしたメイクアップパレットを作成できるパーソナライズドサービスです。 実際にキオスク端末で作成したカラーパレット これまでのPrinkerは個人が所有するtoC向けのデバイスでしたが、Prinker POPは店舗などに導入することを想定しているtoB向けのデバイスです。カメラを通して自身の顔でシミュレーションした色味をメイクアップパレットに出力するものなので、ARメイクの一種と捉えることもできます。 テンポラリータトゥーのPrinkerは日本でも購入できます が、このPrinker POPが日本に上陸するかどうかは未定です。 その他の気になったFashion TechとBeauty Tech ロレアルグループによる「ロレアル セル バイオプリント」 ロレアルグループによるパーソナライズされた肌分析を提供する卓上型ハードウェアデバイス「セル バイオプリント」 出典: https://www.loreal.com/ja-jp/japan/press-releases/group/j-ces-2025/ CES 2024でBeauty系企業として初めてキーノートを開催したロレアルグループは、CES 2025にあわせてパーソナライズされた肌分析を提供する卓上型ハードウェアデバイスの「 ロレアル セル バイオプリント 」を 発表しました 。 このデバイスは、2025年後半にアジア圏で試験的に導入される予定とされています。初出がアジア圏ということには驚きましたが、これは韓国のスタートアップ企業とのパートナーシップによって実現している背景からだと考えられます。近隣の日本でも導入される可能性があるため、注目していきたいと思います。 Withingsによるスマートミラー「OMNIA」 www.youtube.com スマート体組成計をはじめとする健康に結びつくスマートデバイスを提供するWithingsは、CES 2025にあわせてスマートミラー業界に参入して「 OMNIA 」を発表しました。 OMNIAは、体組成はもちろん、あらゆるデータをAIとともに可視化してくれるデバイスです。ブースではまだデモ展示のみでしたが、どんなことができるかはYouTubeに公開されているティザー動画で、どんなことができるかがわかります。 CES 2025ではSamsungも同様にスマートミラー業界に参入して「 MICRO LED Beauty Mirror 」を 発表しました 。以前からスマートミラーは出展されていましたが、2社の参入により今まで以上にBeauty Techとして注目される分野になると考えています。 おわりに ラスベガスの玄関Harry Reid Airport 例によって今回のCES視察は開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 今回は直行便を選択したためフライトのコストはCES 2024当時よりも高くつきましたが、乗り継ぎがない分、時間を有効に使えたと考えています。CES 2026の開催日程は、すでに2026年1月6日から9日の4日間と発表されています。参加意向のある方は、できるだけ早く手配することをおすすめします。 例年通りのことですが、フライトとホテル以外にも一定の金銭的コストが発生しています。CESに限らず、海外で開催されるカンファレンスにおいては、そのコストに対して得られる成果に対するコストの正当性を説明するのは難しいかもしれません。しかし、XR領域は「百聞は一見ならぬ“一体験”にしかず」です。CESに関するニュース記事はCESの会期中から多く目にしますが、現地に足を運び、自らの目と手で体験し、一次情報を得る重要性を再認識しました。 そして、CESはビジネスショーという性質上、個別に会話するプライベートブースが用意されています。いくつか参加しましたが、こういったオンサイトならではの対面コミュニケーションも、インターネットメディアの記事等からは得られない大きなメリットだと考えています。せっかく参加するのであれば、あらかじめそういった場をセッティングしておくことを強くおすすめします。 最後までご覧いただきありがとうございました。来年もまた、CES 2026のレポートをお届けできるように努めてまいります。 ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 現場からは以上です!
はじめに こんにちは、FAANS部フロントエンドブロックでWeb開発をしている 平舘 です。 Webフロントエンドのテスト戦略って、結局どうすればいいのか、よくわからなくないですか? この記事では、FAANS Webアプリケーション開発におけるテスト実装の歴史を「リリース期」「急成長期」「現在」という3つの開発フェーズに分けて振り返ります。プロダクト立ち上げからのリアルな現場感とともに振り返りつつ、主にテスト配分についてチームで議論しながらプロダクトへ反映していった歴史のレポートになっています。みなさんのテスト戦略の見直しや実践のヒントになれば幸いです。 目次 はじめに 目次 背景・課題 この記事で語らないこと 前提:FAANSについて 開発の歴史とテスト戦略の変遷 フェーズ1. 怒涛のリリース期 起きたこと (1) 関心の中心は、「何をつくるか」 (2) こなれない実装 (3) jest-dom による差分チェック (4) テストの目的 (5) QAチームの存在 E2Eテストにおける手動/自動のメリット・デメリット リリース期のまとめ フェーズ2. 疾風怒濤の急成長期 起きたこと (1) 頻発する大規模なリファクタ (2) 統一性のないコードベース (3) 自動テストへの関心 (4) Storybookやるやん (5) Chromaticもええやん (6) 信頼に足るテスト (7) 設計視点の改善 急成長期のまとめ フェーズ3. 現在地とこれから 起きていること (1) 実行コストと信頼性の安定に向けて (2) 俺たちのトロフィー策定 (3)ユニットテストの立ち位置を確認 (4)アプリケーション品質と開発品質を分けて考える 今後の課題 (1) 測定・振り返りへの試み (2) その他 まとめ 背景・課題 現代のWebフロントエンド開発において、テスト手法 1 やツールは多様化しています。ユニットテスト、E2Eテスト、ビジュアルリグレッションテスト(VRT)など、それぞれの用途や目的が異なるため、適切な組み合わせが求められます。しかし、多様な選択肢がある一方で、次のような課題が生じがちです。 フロントエンドのテスト戦略に自信がない テストを書くのがストレス メンバー間でテストへの向き合い方が違う テストの効果を実感しにくい FAANSでも開発フェーズに応じてこのような課題が生じ、向き合ってきました。 この記事で語らないこと こうすればうまくいくという万能な正解や方法論。 テストツールの具体的な使い方。 前提:FAANSについて 内容を理解しやすくするために、まずFAANSのプロダクト背景を簡単に説明します。 サービス特性 : Webは主にバックオフィス機能を提供し、ユーザーの入力・操作が多く発生する。 開発体制 : ウォーターフォール開発。 独立したQAチームが存在。 アーキテクチャ構成 : React SPA : 業務利用ツールのため採用。 OpenAPIによるAPI開発 : YAMLベースでAPIスキーマを定義し、クライアントコードを自動生成。 より詳細なプロダクトの説明は弊バックエンドブロック田島による SLOの導入は早ければ早いほどよい 〜FAANSの事例とその効果〜 - ZOZO TECH BLOG に書かれています。ぜひ併せてお読みください。 開発の歴史とテスト戦略の変遷 以下はざっくりと各フェーズの開発状況を俯瞰した表です。 フェーズ 新規実装 変更・リファクタ QAバグ検出 フェーズ1 リリース期 ↑最大 →少ない ↑最大 フェーズ2 急成長期 ↗︎多い ↑爆増 ↗︎健在 フェーズ3 現在 ↗︎安定傾向 ↗︎多い →安定傾向 これをふまえて、各開発フェーズで起きたことを振り返っていきます。 フェーズ1. 怒涛のリリース期 まずは立ち上げからリリースまでの、プロジェクト最初期のフェーズです。 起きたこと (1) 関心の中心は、「何をつくるか」 どんなプロダクトでもそうだと思いますが、FAANSも立ち上げ当初は、事業価値の心臓となる部分をいかにスピーディに実現するかに集中していました。 将来的な展開を見据えながら、「誰に」「どんな価値を」「どれくらい」「いつまでに」「どうやって」提供するのか。不確実なものばかりのなかで、迅速な意思決定が求められました。 フロントエンドだけに閉じた世界で見ても、フレームワークやライブラリ選定、CI構築、実装指針のすり合わせなどを決める必要がありました。特に機能実装に直結する部分において、アーキテクチャ選定、実装などが山積みでした。 (2) こなれない実装 FAANSのテストの歴史について語るためには、この時期に実装されてしまった こなれない実装 たちについて語らなければなりません。 useEffect() を望ましくない場面で多用し、副作用が複雑に絡み合うことで、とてもテストを書けないようなロジックが量産されていました。 また、TypeScript本来の静的型チェックの効果も十分に発揮できていませんでした。特にnullableな値の扱いが未熟で、冗長なnullチェックによる認知不可が増加していました。 簡単な例としてアカウントの権限管理が挙げられます。FAANSでショップスタッフは必ずどこかのショップに所属しますが、上位権限である管理者アカウントの場合ショップ所属は任意です。これを表現する場合、所属先IDの有無は型で保証されている方が扱いやすいですが、未熟な型表現になってしまっていました。 // 型チェックが効果を発揮できていない type Account = { role : 'staff' | 'manager' ; companyId : number ; // 企業には必ず所属する shopId ?: number ; // ショップは staff のみ所属する } // こっちの方が堅牢で扱いやすい type Account = { companyId : number ; } & { role : 'staff' ; shopId : number ; } & { role : 'manager' ; shopId : number | undefined ; } ※実際の権限はもう少し複雑ですが、わかりやすくするため抽象化しています。 (3) jest-dom による差分チェック 仮想domスナップショットのリグレッションテストも試してみましたが、早々に断念しました。 デザインの方向性を試行錯誤しながら実装を進めている段階だったので、得られるメリットが小さく維持コストのほうが高かったためです。 (4) テストの目的 おそらくこれが最も本質的かつよくある問題ですが、メンバー間で共通認識を揃えられないまま、「ないよりあったほうがいいよね」ぐらいの認識でテストを実装していました。 結果として、 目的地としてどの程度の範囲をどうやってカバーしたいのか 意図がわからない、粒度のバラバラなテスト実装が散在してしまいました。特に、詳細度の高すぎるユニットテストが多く作られてしまいました。変更の影響を受けやすく、この時点で継続的に担保したいアプリケーションレベルの保証材料にならないテストです。これらは完全に無駄なわけではなく目的次第では有効ともとれるものであるため、特に指摘もなく実装され続けていきました。 しかしプロダクトがローンチされていないこの時点では、そもそもデザインや仕様の根幹的な価値自体が担保されていない状況です。末端の入出力、つまり外部仕様の不確実性が大きいなかで、より内部のテストは資産価値の薄いものでした。これらは、のちにメンテナンスコストをかける価値を見出せず削除することになりました。 (5) QAチームの存在 それでも大きな問題なくスケジュール通りにリリースできたのは、QAチームの働きによるものです。頻繁な変更にもかかわらず手動テストでしぶとくテストしてくれました。その作業負荷は相当なものだったと思います、当時のメンバーには頭が上がりません。 この時点でE2Eテストの自動化案も出ましたが、以下のようなメリット・デメリットの観点のうち、特に初期コストを割くのが難しかったため手動テストのみで運用しました。 E2Eテストにおける手動/自動のメリット・デメリット 項目 手動テスト 自動テスト 初期コスト ◯:手動テスト用の環境を用意するのは必須。 ×:環境や外部依存システムに応じた構築が必要。FAANSの場合、外部提供される認証システムとAPIが大きな障壁。 柔軟さ ◯:テストの詳細度や観点を都度コントロール可能。UXの直感的な評価が可能。 ×:静的なテスト詳細が必須。 探索的テスト ◯:得意。 ×:不得意。 再利用性・繰り返し △:テスト対象が増えるほど工数が増えていく。繰り返すことでテスターの心理負荷は増える。 ◯:実装されたテストは資産として反復して利用可能。結果としてカバレッジを大きくしやすい。 開発サイクルへの組み込み ×:開発サイクルへのフィードバックは最後になる。 ◯:CIで開発サイクルに組み込むことが可能。 リリース期のまとめ 目的面・方法面の両方で不確実性に向き合う日々のなか、自動テストに意識を割き構築していくのは、今から考えてもやはり難しく価値が薄かったと思います。思い切って自動テストを「積極的に書かない」判断が必要でしたが、その決定に至る基礎知識がない状態でした。 また、開発健全性という側面で、第三者的な目線から一定期間、開発状況についてのフィードバックをもらう動きをとれていればよかったと思います。現在ZOZOではWebフロントエンドで事業部を横断して情報共有していく動きが増えてきており、こういった問題は改善傾向にあります。 総じて、チームの共通認識に昇華できていない点において、 テストをよく書くメンバー/そうでないメンバー、どちらも同質にテストへの解像度が低かった です。当時を絵にすると、こんな感じでしょうか。 フェーズ2. 疾風怒濤の急成長期 無事ローンチ完了。基幹機能の価値が証明され、さらに機能拡充が進みます。一方で、コードベースの複雑化や、既存コードの大規模な変更が頻発しました。 起きたこと (1) 頻発する大規模なリファクタ 実装指針が徐々にアップデートされ、機能変更が入るタイミングで、設計方針に沿うよう既存実装を大きくリファクタする場面が増えました。 (2) 統一性のないコードベース 人的リソースの都合上、スポットで領域外のエンジニアが参加することも多く人の出入りが多くなり、コード量が増えるとともに複雑さも急増していた時期です。 テスト設計指針が浸透しないまま作成されたコードは、必要機能を満たしていながらも、俯瞰するとテスタビリティを欠いたり認知コストが高かったりしました。テスト指針という観点が抜けていることによって、設計議論の観点が暗黙的に低かったとも見ることができます。 (3) 自動テストへの関心 テスティングトロフィーという概念が話題になり始めたこともあり、この頃ようやくテスト実装指針の明確化に積極的に意識を割き始めました。 (1)、(2)のような状況で、安心して積極的にリファクタしていける環境を整えることが急務でした。 (4) Storybookやるやん そんな折、Storybookに play() 機能 2 が実装されました。 FAANSでもUIカタログとしてStorybookは導入済みであったため、使い慣れたツールでテスト実装できるのは願ってもないことでした。また、UIを通してモック化したAPI通信をテストできるため、信頼性の高い結果を期待できました。 (5) Chromaticもええやん Chromatic 3 を活用して、 play() の実行結果をスナップショットとして保存し、API疎通を含めたUIの整合性を担保しました。これによって、静的なビジュアル差分の確認にとどまらず、インタラクティブな動作を含めた総合的なテスト 4 となりました。 Chromaticは金額によってスナップショットできる上限が決まる料金体系のため、FAANSではPullRequestにラベルを付与することによってトリガーするようにしています。 過去のブログ記事 で実装例も載せておりますので、ご参照ください。 (6) 信頼に足るテスト ツールの学習コスト、MSWによるAPIのモック、CIの整備など、それなりの初期コストはかかりました。しかし、コストに見合うだけの信頼に足るテストをできている手応えがありました。 手応えの理由の1つは、開発サイクルにおいて素早くテスト結果を確認できることです。これまで自分たちの手動テストでしか担保できなかったE2Eに近いレイヤーでの動作保証を、CIでリグレッション検知できるようになりました。 もう1つは、検出した不具合や込み入った仕様を、素早く自動テストに反映できることです。Storybookの play() では実装したインタラクションテストをステップバイステップで簡単にデバッグ実行できます。手元で画面操作を確認しながら書けるテストは、思い描く仕様をスムーズに実装できる優れた機能です。 (7) 設計視点の改善 テストを書く文化が根付いてくると、「テストしやすい実装」も実感を伴ってわかってきます。「これはテストしにくいよね」という観点でスムーズに会話ができるようになり、より疎結合で責務が明確な、よい設計を議論できる土台になりました。 優れた設計はテストしやすい実装となり、信頼に足るテストは自信につながります。リリース期には苦痛ですらあったテストのメンテナンスが、開発体験において欠かせない存在となりました。 急成長期のまとめ StorybookとChromaticという強力なテストツールを使うことにより、自動テストの恩恵を受けることができるようになりました。 早い段階でリグレッションによる不具合を検知することにより、バグを減らすだけでなく、設計の改善や開発体験の向上につながりました。 フェーズ3. 現在地とこれから テスト基盤も少しずつ安定してきました。より効果的なテスト戦略の確立に向け、取り組むべき課題も明確化してきています。 起きていること (1) 実行コストと信頼性の安定に向けて テスト対象が増えるにつれて、1)実行時間の増加と2)Flakyなテストが問題になってきました。様々な改善対策をしましたが、長くなるため、ここではその項目を列挙するだけにとどめます。 実行時間の削減 Storybook shardオプションによる並列ジョブ実行 Chromatic turboSnapオプションによる差分検出 Flakyなテストの安定化 Storybookのバージョンを上げるタイミングで、記法が古いテストとFlakyなテストは一旦すべてコメントアウトした ボーイスカウト精神で直していくようにした asyncUtilTimeout オプションにより findBy のデフォルトタイムアウト時間を延長 これらの取り組みによって、現状ではStorybookとchromaticの実行時間とFlakyさは改善し、ほぼネックになっていないため、信頼性の高いテストを十分な速さで実行できている安心感があります。 (2) 俺たちのトロフィー策定 play() とChromaticによって結合テストあたりのレイヤーを厚くする方針は決まっています。しかしテストの目的・達成したい状態をより明確にチームで共有したいところです。 そこで、チーム全員で 「自分たちのテスト配分」 を描くことで、テストの目的や役割について議論を深めました。これはどのツールでどの層を、そしてどんな品質を担保するのか、解像度を上げるきっかけとして非常に有効でした。 左が初期案、右が完成系です。細かいですが配置の調整や、ツールについて明示しました。 完成に至るまでのプロセスでは、以下のような重要な議論が行われました。 (3)ユニットテストの立ち位置を確認 議論の結果、ユニットテストは以下のような範囲に限定する方針を採用しました。 外部依存のない静的ロジックを説明・定義すること。 たとえば、計算やフォーマット変換のようなロジック。 ドメインロジックやUIに密接な部分は結合テストに委ねること。 Storybookの play() 機能やChromaticでカバーすることで、手動操作に近いレベルでの信頼性を確保します。 コードベースは、テスタブルなコードとそうでないものが混じった状態です。理想は、テストしやすいロジックに設計を見直すことですが、リソースが限られている現状では現実的ではありません。まずは機能レベルで壊れにくい状態を目指すことが最優先と判断しました。 (4)アプリケーション品質と開発品質を分けて考える CIで自動テストを回すことで、漠然と「ある程度の品質保証はできているはず」という感覚に頼っていましたが、そもそもここでいう「品質保証」とは何を指しているのかをチーム内で議論しました。 この議論の中で、 テスティングトロフィー を意識しすぎた結果、固定観念に囚われてしまいがちだったことが明らかになりました。具体的には、ユーザに届けられる「アプリケーション品質」と、QAテストに引き渡す段階やリリースサイクル全般で必要とされる「開発品質」を分けて考える視点が不足していたのです。この視点によって、品質保証におけるチームのアプローチを整理できました。 FAANSではアジャイルな手法を取り入れることで、各工程においてバグ検知や仕様変更などのフィードバックを柔軟に実装に反映させることができる開発サイクルになっています。しかし、基本的には静的な仕様からQAテストを経てリリースするウォーターフォール方式であるため、この 品質の境界を明確に認識することが重要である という結論に至りました。 現状、開発チームのテストが直接的に保証できるのは主に開発品質です。一方で、最終的なアプリケーション品質を担保するにはQAテストが欠かせません。この事実整理によって、現状のフロント開発が担保しているテストだけでは不十分だということを再確認しました。ただし、もちろんこれは開発チームがアプリケーション品質を負わないということは意味しません。この区別の明確化により、 テスト実装の目的とその効果の測定範囲 を整理し、より適切なテスト戦略を構築できるようになります。そして、次のステップとして「開発品質」と「アプリケーション品質」をそれぞれ強化するためのアプローチを模索しています。 今後の課題 (1) 測定・振り返りへの試み さて、テスト結果(主にカバレッジ)を測定・振り返りすることで開発品質を上げ、より「根拠のある自信」 5 をもった開発体験にしたいところです。 しかし、フロント開発に閉じた世界で 定量的に品質を振り返る指標 を定めるのが難しいため、測定の設計が難航しています。以下のような方針で測定の準備を進めています。もしもよりよい方法があればぜひ教えてください。 機能軸で優先度づけし、ページ単位で観測。 事業重要度やユーザ利用頻度に基づいて優先度を定義。ページコンポーネント単位でカバレッジを観測。 優先度が高いページのカバレッジが落ちないようにする。 優先度が高いのにカバレッジの低い箇所を改善していく。 SonarQube Cloudの活用 静的観測軸として、コードベースの健康状態を監視。 複雑さの増大を監視し、テストしやすさの低下を未然に防ぐ指標として活用。 モジュール軸でのカバレッジ測定対象の定義 複雑性や変更頻度を考慮した重み付けし、効果的な測定対象を絞り込む。 今後また、結果を公開できればと考えています。 (2) その他 冒頭でご紹介した通り、FAANSではSLOにより品質を監視しています。組織全体でアプリケーション品質を維持するため、フロントエンドとしてどのようにこの指標を活用・コミットできるか模索しています。 QAチームにより本質的なテストに注力してもらうためには、E2Eを一部自動化し効率化したいところです。開発・QAで連携し、より良い開発サイクルを生むための施策を協議していきます。 まとめ フロントエンド開発における自動テストのあり方は、チームの成長やツールの進化とともに動的に変化していきます。今後生成AIの進化も大きく影響していくことでしょう。ベストプラクティスを静的な理想形として求めるのではなく、自分たちに合った戦略を築き上げる意識を持ち、動的で可塑性のある開発プロセスが重要だとわかりました。 今回の振り返りを通じて得られた学びは以下の通りです。 チーム全体での目的共有 テストの役割や目的を明確にし、同じ方向に向かって改善していくことで、安心感・納得感が高まること。 開発フェーズに応じた柔軟な戦略 特に黎明期は避けられない障害が多くある一方で、基礎知識と経験によって、ツールや環境の特性に則した戦略をとれること。 信頼性の可視化と測定の取り組み 現在は主要機能やロジックの重み付けを通じて適切な指標を模索しています。取り組み中ではありますが、信頼性向上の大きな一歩になると考えています。 現在進行形の取り組みが多い中ですが、このプロセス自体がプロダクトやチームの成長につながると信じています。今回の記事が同じ課題に向き合うみなさんの一助となれば幸いです。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 特に前置きなく「テスト」と指す際は、TDDなどの設計・実装手法ではなく自動テストの文脈で語っています。 ↩ StorybookのPlay-functionは、ユーザーの操作をシミュレートし、コンポーネントのインタラクションを自動的にテストするための機能です。 Docs | Storybook ↩ Chromaticは、Storybookと統合されたビジュアルリグレッションテスト(VRT)ツールで、UIの見た目の変更をスナップショット比較で検出します。 Visual testing & review for web user interfaces • Chromatic ↩ 関連記事 Visual E2E Testing with Chromatic and Playwright ↩ 関連記事 ピラミッド、アイスクリームコーン、SMURF: 自動テストの最適バランスを求めて / Pyramid Ice-Cream-Cone and SMURF - Speaker Deck ↩
ZOZO開発組織の1か月の活動をまとめたMonthly Tech Reportをお届けします。2024年12月はアドベントカレンダーやGirls Meet STEMなどのイベントの他、様々な場で登壇しました。そして、2024年12月をもって、ZOZOTOWNは20周年を迎えました。そんな12月の出来事をご紹介します。 ZOZO TECH BLOG 2024年12月には、前月分のMonthly Tech Reportを含め15本の記事を公開しました。その中でも特に注目度の高かった記事をピックアップしてご紹介します。 12月20日に公開した「AWS re:Invent 2024参加レポート」は、AWS re:Invent 2024に参加したZOZOのエンジニアが、その概要や注目ポイントをまとめた記事です。例年、多くの新サービスが発表されるAWS re:Invent。今年も、その様子を詳しくお伝えしています。 techblog.zozo.com また、ZOZO発のOSSとしてリリースした「universal-links-test」に関する記事を公開しています。「universal-links-test」は swcutil コマンドをラップした関数を提供し、 apple-app-site-association ファイルの挙動確認をサポートするものです。開発にあたってのモチベーションや利用方法などを紹介しています。 techblog.zozo.com 登壇 SIGGRAPH Asia 2024 Tokyo 12月3日から6日の4日間に渡って開催された『 SIGGRAPH Asia 2024 』に、ZOZO 生産研究開発部 シミュレーションブロック ブロック長の安東と、ZOZO NEXT ZOZO Research Dept Applied ML Teamの平川の論文がそれぞれ採択されました。 コンピュータグラフィックス分野のトップカンファレンス 「SIGGRAPH Asia 2024」にて論文採択 ZOZO研究所、コンピュータグラフィックス分野のトップカンファレンス「SIGGRAPH Asia 2024」にて論文採択 会期中、安東はTechnical Papers Fast Forward、Technical Papers、Digging into the Technical Papersの各セッションで、平川はTechnical Communicationsで登壇しました。 ZOZO 安東 「 A Cubic Barrier with Elasticity-Inclusive Dynamic Stiffness 」 「 Digging into the Technical Papers 」 ZOZO NEXT 平川 「 An Empirical Analysis of GPT-4V’s Performance on Fashion Aesthetic Evaluation 」 Miroマスターズ 2024 12月5日に開催された『 Miroマスターズ 2024 』で、技術戦略部 テックリードの堀江( @Horie1024 )が「 生産性を倍増せよ! 11人のMiro達人たちの仕事活用術 」の枠において「 Miro × ZOZO ZOZOのMiro活用事例紹介 」というタイトルで登壇しました。 speakerdeck.com AI Leaders Connect #2 12月5日に開催された『 AI Leaders Connect #2 』で、AI・アナリティクス本部の川田が「 ZOZOの生成AI業務活用事例 」というタイトルで登壇しました。 01(zeroONE)2024 12月10日に開催された『 01(zeroONE)2024 』で、データシステム部の奥山が「 【クリエイティブサーベイ / ZOZO】今年のデータ基盤振り返り大会 」の枠において「 ZOZOにおけるデータマート集計基盤の成長と反省 」というタイトルで登壇しました。 📰ZOZOエンジニア登壇情報 本日、東京ミッドタウン・ホールで開催中の 01(zeroONE)2024 にデータシステム部 データ基盤ブロックのデータエンジニア 奥山が登壇します🎙️ 🖥️今年のデータ基盤振り返り大会 📅 2024/12/10 13:40 - 14:10 EXPO Theater A https://t.co/A3gxAOp4gY #01pN — ZOZO Developers (@zozotech) 2024年12月10日 speakerdeck.com 奥山は社会人漫才師「下町モルモット」のぽこやかざん( @pokoyakazan )としても活動しています。過去にはType転職の「 聴くエンジニアtype 」にも出演しています。興味のある方はぜひチェックしてみてください。 #075 ZOZOのデータエンジニア 兼 お笑いやってます/ZOZO奥山さん① #076 仕事の辛い・苦しいから解放してくれるのは“メタ反省会”/ZOZO奥山さん② #077 1日1%の成長を目指す。愚直にやれば、いつか何かしらの成果に繋がる/ZOZO奥山さん③ 株式会社ユーザベース×株式会社ZOZO×株式会社PR TIMES 3社合同フロントエンド勉強会 12月10日に開催された『 株式会社ユーザベース×株式会社ZOZO×株式会社PR TIMES 3社合同フロントエンド勉強会 』で、WEARフロントエンド部 テックリードの冨川( @ssssotaro )が「 useSyncExternalStoreを使いまくる 」というタイトルで、ZOZOTOWN開発3部の田中( @nayuta999999 )が「 MSW 2.xにあげた話 」というタイトルで登壇しました。 本日 12/10 (火) 開催!『株式会社ユーザベース×株式会社ZOZO×株式会社PR TIMES 3社合同フロントエンド勉強会』にZOZOエンジニアが2名登壇します🎙️ 🗣️ ZOZOTOWN開発3部 田中 @nayuta999999 🗣️ WEARフロントエンド部 テックリード 冨川 @ssssotaro https://t.co/afhkAscy6N #zup_frontend — ZOZO Developers (@zozotech) 2024年12月10日 speakerdeck.com speakerdeck.com ZOZO TECH BLOGとPR TIMES 開発者ブログにイベントレポートが掲載されています。こちらもぜひご覧ください。 techblog.zozo.com developers.prtimes.jp そのリプレイスは最適解? -コストから見るプロダクト開発Tips 12月11日に開催された『 そのリプレイスは最適解? -コストから見るプロダクト開発Tips 』で、物流開発部の上原が「 本番環境での等価比較がコスト削減に繋がった話 」というタイトルで登壇しました。 【ZOZOエンジニア登壇情報】 12/11(水) 12:00~13:00 にオンラインで開催される『そのリプレイスは最適解? -コストから見るプロダクト開発Tips』に物流開発部で基幹リプレイスに携わっている上原が登壇します🎙️ ぜひお気軽にご参加ください! https://t.co/ybVHmC8ipg #コストリプレイス_findy — ZOZO Developers (@zozotech) 2024年11月21日 speakerdeck.com GitHub Universe 2024 Recap in ZOZO 12月16日に開催された『 GitHub Universe 2024 Recap in ZOZO 』で、WEARフロントエンド部の山田( @gamegamega_329 )が「 iOS開発におけるCopilot For XcodeとCode Completion 」というタイトルで、データシステム部の佐藤( @rayuron )が「 GitHub Copilot のテクニック集 」というタイトルで登壇しました。 先日のイベントで @rayuron さんが発表されていたGitHub Copilotのテクニック集スライド! 🏃‍♀️ショートカット 🔃プロンプティングの技 💡仕様に基づいた知恵 ✍そして「少し書き始める」 実用的で分かりやすくまとめられています。 是非チェックしてみてください👇 https://t.co/RzYWQbmfje — GitHub Japan (@GitHubJapan) 2024年12月18日 先日のイベントからもう1つ、発表スライドの紹介です! iOS開発におけるGitHub Copilot For XcodeとCode Completion! iOS開発で2つのツールのコード補完について試してみた 🔰新規でコードを追加 ✅テストコードを追加 🍎AppleのSDKを扱う ヒント:適材適所 結果は👇 https://t.co/82DtXhiEGK — GitHub Japan (@GitHubJapan) 2024年12月18日 イベントレポートもあわせてご覧ください。 techblog.zozo.com AWS re:Invent 2024 Recap in ZOZO 12月17日に開催された『 AWS re:Invent 2024 Recap in ZOZO 』に、AWS re:Invent 2024に現地参加した4名のZOZOエンジニアが登壇しました。 📰ZOZOエンジニア登壇情報 明日 12/17 (火) 夜開催の自社イベント『AWS re:Invent 2024 Recap in ZOZO』に * 計測システム部から纐纈と土田 ( @andex_tokyo ) * 技術本部 SRE部から佐藤 ( @taquaki_satwo ) と江島 ( @sejima1105 ) 以上4名が登壇します🎙️ https://t.co/m27BlytrTp #reInvent24Recap — ZOZO Developers (@zozotech) 2024年12月16日 纐纈: EKSとAmazon Qのアップデート 土田: Amazon Novaのすゝめ基盤モデルの性能比較を添えて 佐藤: 英語が苦手でも学びが得られるWorkshopについて 江島: ガバナンスを支える新サービス イベントレポートには、ゲストスピーカーとしてご登壇いただいたアマゾンウェブサービスジャパン合同会社3名の方の登壇資料も掲載しています。あわせてご覧ください。 techblog.zozo.com 宣伝会議AI研究会 12月18日に開催された『 第1回 宣伝会議AI研究会 』で、AI・アナリティクス本部の川田が「 生成AIが実現するZOZOの業務効率化 ー 実例に見る活用の可能性 」というタイトルで登壇しました。 第47回 MLOps 勉強会 12月18日に開催された『 第47回 MLOps 勉強会 』で、データシステム部の佐藤( @rayuron )が「 ZOZOTOWN の推薦における KPI モニタリング 」というタイトルで登壇しました。 本日 12/18(水) 19~20 時にオンラインで開催される『第47回 MLOps 勉強会』にデータシステム部 推薦基盤ブロックの佐藤 @rayuron が『ZOZOTOWNの推薦のKPIモニタリング』というタイトルで登壇します🎙️ ご興味をお持ちの方はぜひご参加ください! https://t.co/VrFlFt0qvB #mlopsコミュニティ — ZOZO Developers (@zozotech) 2024年12月18日 speakerdeck.com 掲載 流通ニュース ZOZOの生成AI活用事例に関する記事が、小売・通販・中間流通・メーカーの最新ビジネスニュースを発信する「 流通ニュース 」に掲載されました。 www.ryutsuu.biz これらの活用事例は「“ビジネスAI元年” の2024年・ZOZOの生成AI活用事例」としてZOZO DEVELOPERS BLOGにもまとまっています。こちらにはAI・アナリティクス本部 本部長 牧野のコメントも掲載されています。あわせてご覧ください。 technote.zozo.com 日本ネット経済新聞 11月22日から12月13日にかけて実施していた「GitHub × ZOZOTOWN コラボアイテム販売」の取り組みについて、技術戦略部 ディレクターの諸星( @ikkou )が取材を受けた記事が、EC&流通のデジタル化をリードする専門紙「 日本ネット経済新聞 」に掲載されました。 netkeizai.com MONOist ZOZOMATやZOZOMETRYなどの計測技術に関する取り組みについて、計測プラットフォーム開発本部 本部長の山田が取材を受けた記事が、モノづくりスペシャリストのための情報ポータル「 MONOist 」に掲載されました。 monoist.itmedia.co.jp FASHIONSNAP ファッションコーディネートアプリ「WEAR by ZOZO」による「 WEAR Coordinate Awards 2024 」の取り組みについて、ファッションに関連する事象を中心に様々なトピックを取り上げる情報サイト「 FASHIONSNAP 」に掲載されました。 www.fashionsnap.com 「WEAR Coordinate Awards 2024」の特設サイトもあわせてご覧ください。 wear.jp その他 ZOZO Advent Calendar 2024 実施 2024年のアドベントカレンダーは、過去最多となる全11シリーズ、275記事を公開しました。 techblog.zozo.com ZOZO NEXT が XR Kaigi 2024 に初出展 12月12日、13日の2日間に渡って催された XR Kaigi 2024 のエキスポエリアにZOZO NEXTが初出展しました。 【 #XRKaigi 2024に出展】 東京ポートシティ竹芝で開催されるXR・メタバースの国内最大級のカンファレンス「XR Kaigi」に、ZOZO NEXTが2024年12月12日(木)~13日(金)に出展します。 https://t.co/vTlptizXsh https://t.co/z1l9fSVYbf — 株式会社ZOZO NEXT (@ZOZONEXTInc) 2024年12月11日 Girls Meet STEM 開催 12月15日(日)に、ZOZOにて中高生女子を対象とした体験イベント「 Girls Meet STEM〜ITのお仕事を体験しよう〜 」を開催しました。 techblog.zozo.com 以上、2024年12月のZOZOの活動をお届けしました! ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに 技術評論社様より発刊されている Software Design の2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。 これまでの連載で、ZOZOTOWNリプレイスプロジェクトの始まりから各部門の取り組みなどを紹介してきました。最終回となる今回は、フロントエンドの取り組みを取り上げ、これまでのまとめを行います。 目次 はじめに 目次 はじめに フロントエンドエンジニアの責務 フロントエンドリプレイス前 リプレイス後 フロントエンドリプレイスプロジェクトの進め方 調査 設計 テスト Next.jsとの向き合い方 Custom Server 1. ロギング 2. リダイレクト 3. 既存システムからのForm POSTリクエストを受けるエンドポイント 4. マルチプロセスでの起動 next/link カナリアリリースと_next/data/*/jsonの関係 Next.jsでリプレイスしていくうえでの課題 ソフト/ハードナビゲーションのHTTPリファラの違い Shift_JISの取り扱い 振り返り リプレイス後のフレームワークとしてのNext.js リプレイス専任チームにした話 不要機能の削除・調整 リリース まとめ・今後の展望 はじめに ZOZOTOWNのWebフロントエンドは約3年前からリプレイスを実施してきました。連載最終回となる今回はZOZOTOWNのWebフロントエンドリプレイスプロジェクトの進め方や、その過程で得られた技術・組織に関する知見について紹介します。全8回にわたる連載のまとめとして、リプレイスプロジェクトの今後の展望についてもお伝えします。 フロントエンドエンジニアの責務 当社におけるフロントエンドエンジニアの役割について、リプレイス前後のアーキテクチャを比較しながら紹介します(図1)。 図1 フロントエンドリプレイスのアーキテクチャ遷移 フロントエンドリプレイス前 Windowsサーバ上で動作するIIS(Internet Information Services)でClassic ASP(VBScript)を利用して動的にHTMLを生成するサーバレンダリングを行っています。 HTMLの生成にはClassic ASP(ロジック)とHTML(テンプレート)の分離を可能にするテンプレートエンジンを利用しています。また、ブラウザ上ではJavaScriptライブラリのjQueryと一部React(TypeScript)を利用してインタラクティブなコンテンツの実装を行っています。 フロントエンドエンジニアの役割はリスト1に示されるように、Classic ASP(VBScript)で書かれたコード以外のHTML、CSS、JavaScriptを担当することです。 ▼リスト1 テンプレートファイル <!DOCTYPE html> < html > < head > < meta charset = "Shift_JIS" > < link rel = "stylesheet" href = "/assets/style/index.css" > </ head > < body > < header > (#%NoticeExists| < div class = "badge" > < div class = "badge-circle--count" > (#*UnreadNoticeCount#) </ div > </ div > #|# #) </ header > < div id = 'react-app' ></ div > < script src = "/assets/script/index.js" charset = "utf-8" ></ script > </ body > </ html > リプレイス後 IISとClassic ASPで実装していた部分は、Next.js(Pages Router)とUIに必要なデータをマイクロサービスなどから集めて整形するBFF(Backend for Frontend)に分解されました。その結果、フロントエンドエンジニアはNext.js、バックエンドエンジニアはBFFと、管理する役割がサーバ単位で分割されました。 Next.jsを導入したことで、フロントエンドエンジニアの役割にいくつかの変化が生じました。Next.jsはサーバサイドレンダリング(SSR)や静的サイト生成(SSG)をはじめWebアプリケーションに必要な機能を提供します。そのためフロントエンドエンジニアとしてページルーティング、HTMLのキャッシュ管理、機能要件・SEOを考慮したレンダリングパターンの選定などの役割が増えました。また、機能要件・SEOを考慮してSSRを利用するためサーバのパフォーマンスやエラーなどのメトリクスを監視し、サーバの運用を行うことも求められるようになりました。リプレイス前と比較して、フロントエンドエンジニアはサーバを含めた技術をより一層駆使してユーザーに快適なWeb体験を提供できるようになりました。 フロントエンドリプレイスプロジェクトの進め方 ZOZOTOWNは2004年のサービス開始から複数の技術で構成され、多くの開発者が機能改修を行ってきたことで、機能同士の依存関係が複雑になっていました。その中で開発当時の設計意図を直接的には知らないリプレイス専任のチームがどのようにZOZOTOWNのWebフロントエンドをリプレイスするプロジェクトを進めていったかを紹介します。 リプレイスプロジェクトはページや機能ごとにいくつかのフェーズに分けて進行します。各フェーズは通常の開発工程(調査・設計・開発・テスト・リリース)に従って進行しますが、リプレイスプロジェクト特有の課題が多くあります。とくに注意が必要な工程について説明します。 調査 長く運用され変遷を遂げてきたZOZOTOWNには機能要件仕様書が存在しないため、稼働しているコードに記載されているものが仕様であり、要件でもあります。リプレイスプロジェクトの基本的な要件は既存システムの要件を漏れなくリプレイスすることです。そのため、要件をコードから読み解く調査がとても大事な工程となります。 調査工程では、既存システムの機能開発・保守をしているチームではないため機能の理解に時間がかかるという課題があります。また、IISとClassic ASPで実装されている部分をどのようにフロントエンド/バックエンドで分けてリプレイスを行うかの判断も必要です。そしてフロントエンドエンジニアがバックエンド技術で実装されている機能についても理解する必要があることが課題となります。 これらの課題に対して、IISとClassic ASPで実装されている機能の一覧、通信シーケンス図、画面遷移図を作成し、開発者が既存機能の要件・機能を理解できるようにしています。また、機能を一覧にすることでフロントエンド/バックエンドエンジニアどちらが実装するかを漏れなく判断し、設計後のフェーズでの実装漏れによる後戻りを防ぐようにしています。 設計 調査で作成した機能一覧を元に設計していきます。リプレイス後はモノリスではなくNext.jsとBFFのため、OpenAPIを使ったスキーマ設計、通信シーケンス図を作成することを行いフロントエンド/バックエンドそれぞれが独立して開発を進めていけるようにします。 レンダリングパターンについては「SEO観点で劣化しないことを確約できる変更以外はしない」というプロジェクトポリシーに沿って基本的に既存と同様にします。また、リプレイス対象ページによっては現在のアーキテクチャ設計時点でのコアの実装を行い、レイテンシーやファーストビューなどのパフォーマンス劣化を起こさずにリプレイスできるかどうか先行して検証するためにProof of Concept(PoC)を行うことがあります。 テスト リプレイスは不具合が発生すると多くのユーザーに影響が出てしまいます。そのリスクを最小限に抑えるために、一部のユーザーだけにリプレイス後のシステムを提供するAkamai Application Load Balancerを利用したカナリアリリースを実施しています。提供する割合を徐々に増やしていき、全ユーザーに提供するまでに不具合が見つかった場合は提供割合を0%に戻して不具合を修正します。そのためリリース時のユーザー体験だけでなく、リリースを戻す際のユーザー体験に影響がないかもテストする必要があります。 Next.jsとの向き合い方 Custom Server ZOZOTOWNではCustom ServerにWebフレームワークのFastifyを利用してNext.jsを起動しています。Fastifyは一般的に使われるNode.jsフレームワークのExpressよりも高い処理速度を持ち、Hooks APIにより複数用意されているライフサイクルイベントをフックして処理を簡単に実行できます。Custom Serverで行っている処理は次の4つです。 1. ロギング FastifyのonResponseイベントをフックにしてサーバのアクセスログを出力しています。出力にはライブラリpinoを利用してJSON Lines形式で標準出力しています。 2. リダイレクト ZOZOTOWNはデスクトップ向けとモバイルデバイス向けで別々のURLが存在します。そのため、モバイルデバイスでデスクトップ向けURLにアクセスがあった場合はモバイルデバイス向けのURLにリダイレクトする仕様があります。また、既存システムではURLにソース情報である.htmlが含まれており、リプレイスで.htmlなしのURLに変更するためリダイレクトを行います。 Next.jsの機能としてのRedirect、Middlewareを利用することも検討しました。しかしRedirectは柔軟な条件設定が難しく、Middlewareはリプレイス当初のNext.jsバージョンではExperimentalな機能であったため、Custom Serverでリダイレクトを行っています。 3. 既存システムからのForm POSTリクエストを受けるエンドポイント リリーススコープを限定してリスクを最小限に抑えるため、既存システムでForm POSTリクエストを送っている箇所とリプレイス後のシステムの連携が必要なことがあります。ただし、Next.js(Pages Router)の getServerSideProps ではForm POSTのbodyをパースするしくみがないため、Fastifyにリクエストを受けるエンドポイントを作成することでパースされたPOST bodyの取り扱い処理を行っています。 4. マルチプロセスでの起動 とあるページのリプレイスでPoCを行った際に現在のサーバ性能・台数ではリクエストをさばききれないことがわかりました。単純に台数を増やすだけではかなりのコストがかかるため、サーバのCPUリソースをできる限り使ってさばけるリソースを増やすためにマルチプロセスで起動する処理を実装しています。 next/link next/linkはNext.jsでクライアントサイドのナビゲーションを実現するためのコンポーネントです。next/linkはページ遷移を行う際、サーバからHTMLではなくページを構成するデータ(json)を取得し、クライアントサイドでページを構築します。そのためページ全体を再読み込みするのではなく必要な部分だけを更新できるので、ユーザーにとってストレスのないページ遷移を実現できます。 リプレイスプロジェクト開始当初は、リプレイスされるページ間の遷移をクライアントトランジションでシームレスにすることを目指していました。ZOZOTOWNではバックエンドでURLを決定するロジックが多く、動的にURLが変わることがよくあります。そのため、Next.jsでリプレイス済みのURLの場合はnext/linkを使い、リプレイス前のURLの場合はaタグを使うAnchorコンポーネントを作成しました。コンポーネント化することで開発者がnext/linkを意識せずにできる限りクライアントサイドでの遷移になるようにしています。 Next.jsでリプレイス済みのURLか否かは、/pagesに存在するページのパスを生成してくれるpathpidaを利用してpropsのhrefと比較することで判定しています(リスト2)。 ▼リスト2 Anchor コンポーネント import { ReactNode } from 'react' import Link from 'next/link' const Anchor = ( { href } : { href : string , children : ReactNode } ) => { const isDefaultAnchor = isNextApplicationPath(href) if (isDefaultAnchor) { return < a href = { href } > { children } </ a > } return ( < Link href = { href } passHref > < a href = { href } > { children } </ a > </ Link > ) } カナリアリリースと_next/data/*/jsonの関係 ZOZOTOWNリプレイス後の環境では、新バージョンのリリースに伴うリスクを低減するために、新・旧バージョンを段階的に切り替えるカナリアリリース(エラー件数が多い場合は自動で0%にロールバック)が採用されています *1 。このため、リリース中はバージョンスキューのため新・旧の通信が入り混じることで _next/data/*/json が404エラーになることがあります(図2)。また、リプレイス後の環境でブラウザを開いたままにしていて新バージョンのリリース後にクライアントサイドトランジションを行った場合も _next/data/*/json が404エラーになります。 図2 Version Skewでの通信 404エラーになることによるユーザー影響を懸念しましたが、Next.js側で別のバージョンの不一致が発生した場合はアプリケーションを再読み込みするハードナビゲーションを行うしくみとなっているため、ユーザーには影響がないことが確認されました。これによって無事、リプレイス後の環境からリリースのリスク低減のためのカナリアリリースを導入することができました。 Next.jsでリプレイスしていくうえでの課題 ソフト/ハードナビゲーションのHTTPリファラの違い ソフトナビゲーションはhistoryを使用してURLを変更後にページに必要なデータ(json)を取得するため、ブラウザバックを行うと戻ったURLがリファラとなります。一方、ハードナビゲーションはページ全体が再読み込みされるため、戻る前の現在のページのURLがリファラとなります。 ZOZOTOWNでは、流入経路によって特殊なUIを表示する仕様や、前の選択状態を維持するためにリファラを利用していました。この状態でhistoryを使用したソフトナビゲーションにすることでブラウザバック時に問題が発生しました。 リファラは状況によってHTTPに乗らないこともあるため、依存しない実装に変更することも検討しました。しかし、この問題が発覚したタイミングがリリース直前で利用箇所も多く要件をまとめて設計することが難しかったため、問題が発生する特定のページからの遷移と特定ページへの遷移をハードナビゲーションに変更する対応を選択しました。 Shift_JISの取り扱い ZOZOTOWNのサービス開始以降、現在もWindows Server上でIISとClassic ASP(VBScript)が稼働しています。その結果、システムには文字コードShift_JISが残っており、キーワード検索のURLクエリにもShift_JISでエンコードされた値が使用されています。リプレイス後も裏側のシステムは引き続きShift_JISでの処理を行うので、互換性維持のためShift_JISを扱う必要があります。しかし、Shift_JISでエンコードされたマルチバイト文字を含むURLに対してNext.js(JavaScript)でURLSearchParams APIを使用すると、 application/x-www-form-urlencoded 形式でパースされてしまい文字化けしてしまうため、Shift_JISのまま扱うことができません。これは2つのユースケースで問題が発生しました。 1つ目はページネーションやソート順の変更など現在のURLに対して特定のクエリパラメータを変更したい場合です。Shift_JISでエンコードされたマルチバイト文字を含むパターンに対しては、URLSearchParams APIが登場する前のやり方と同じようにURL文字列を操作することで対応しました。 2つ目は getServerSideProps でリクエストURLを参照したい場合です。リプレイス後は基本的にソフトナビゲーションで実装しているため、リクエストオブジェクトのURLではなくクライアントサイドナビゲーションの _next/data を正規化した GetServerSidePropsContext のresolvedUrlを参照する必要があります。しかし、resolvedUrlはNext.js内部でURLSearchParamsを利用しているため文字化けしてしまいました。この問題に対してはNext.js内部での処理によって文字化けが発生してしまうことがわかりハードナビゲーションに変更することを検討しました。検討した結果、問題が発生するケースの中に既存システムからソフトナビゲーションでCSRを行っている箇所があったため、このケースのみresolvedUrlを利用せずほかのケースはリクエストオブジェクトのURLを利用することで対応しました。 また、HTMLの文字コードをShift_JISからUTF-8に変更したことでも問題が発生しました。FormデータのエンコーディングはHTMLの文字エンコーディングに依存するため、リプレイス後も送信先が既存システムの箇所でUTF-8がShift_JISとして扱われることで文字化けが発生しました。Formはaccept-charset属性を指定することでエンコーディングを指定できるため、Shift_JISを指定することで文字化けの問題を解決して新・旧システムを連携できました。 振り返り リプレイス後のフレームワークとしてのNext.js 1ページをピックアップし、Core Web Vitalsを使ってリプレイス前・後のシステムのパフォーマンス特性を比較しました。リプレイス後はTime to First Byte(TTFB)、First Contentful Paint(FCP)が改善されたことで、Largest Contentful Paint(LCP)までの時間短縮やTime to Interactive(TTI)が全体的に向上しました。一方でFCPとLCPの差が広がりページのレンダリングプロセスが遅くなっているため、今後の改善課題であることがわかりました。 開発者体験としては環境構築が簡単になったことや、JavaScriptのエコシステムを利用できることで開発効率が向上したことが挙げられます。また、表示ロジックがすべてJavaScript(TypeScript)で記載されることでテストがしやすくなったことも成果です。 一方でリプレイスならではの課題として既存システムとの共存があります。基本的に既存システムの機能仕様を変更する判断は行わずにリプレイスするため、現在のベストプラクティスと異なりフレームワークでサポートされていないことが数多くあります。そういった場合にフレームワークの制限の中で再現する必要性が挙げられます。 リプレイス専任チームにした話 プロジェクトが始まったころはチーム内で既存システムでの開発とフロントエンドリプレイスを並行して行っていました。プロジェクトとシステムを行き来しコンテキストスイッチを繰り返す必要があり、よりスムーズな進行を目指して専任チームで進めることとしました。そうすることで、スイッチする機会を減らしリプレイスプロジェクトに完全に集中できる環境を整えられました。 リプレイスを進め環境がモダンになっていく中で開発効率も上がり新しい人材も増え、現在ではフロントエンドリプレイスプロジェクトに3チームで並行して取り組めるようになりました。 不要機能の削除・調整 長く運用されてきたこともあり、既存システムにはデッドコードになっているものや古い機能のまま更新されずにいるものが数多くありました。リプレイス後のシステムになるべく負債を残さないように、また本来達成したいシステム入れ替えに大きく影響を与えないように、UIの刷新や機能の削除も積極的に関係者と調整して実施しました。 大きく複雑なシステムのため削除の判断がつかないものや、既存システムと並行して運用した際に問題がある場合など、その時点での判断を見送ったものも多くありますが、既存システムよりだいぶシェイプアップできました。 リリース 現在ZOZOTOWNのフロントエンドでは、ページや機能単位でリプレイスを行っています。既存システムと並行して開発していく関係で二重開発になってしまうケースや、リプレイスが完了すれば不要になる既存システムと整合性を保つための処理の開発などコストがかかっている場面もありますが、ビッグバンリリースによるリスクと天秤にかけて選択しています。 アプリケーションまるごとのリプレイスはしていないものの、ページによっては非常に複雑で大規模なリプレイスになってしまい、結果として非常に苦労することもありました。機能ではなくURL単位でリプレイスするなど小さくリリースしていく手段を複数持ち、適切に提案・判断できる状態にある必要性を感じました。 まとめ・今後の展望 これまで全8回にわたって、ZOZOTOWNリプレイスプロジェクトにおける取り組みや学びをさまざまな切り口で、紹介しました。 第1回:ZOZOTOWNリプレイスプロジェクトの全体アーキテクチャと組織設計 第2回:ZOZOTOWNリプレイスにおけるIaCやCI/CD関連の取り組み 第3回:API Gatewayとサービスメッシュによるリクエスト制御 第4回:ZOZOTOWNリプレイスにおけるマスタDBの移行 第5回:キャパシティコントロール可能なカートシステム 第6回:ZOZOTOWNにおけるBFFアーキテクチャ実装 第7回:検索機能リプレイスの裏側 第8回:フロントエンドエンジニアから見るZOZOTOWNリプレイスとまとめ・今後の展望 これらは、壮大なZOZOTOWNリプレイスプロジェクトの一部です。筆者たちは日々、試行錯誤を繰り返し、ZOZOTOWNという巨大なサービスのリプレイスに取り組んでいます。 ZOZOTOWNは、2004年12月のサービス開始から、基本的なアーキテクチャを変えずに成長してきました。そのアーキテクチャはきっと正解だったのだと思いますし、リプレイスに至るまで、開発や運用を続けてきたZOZOのエンジニアをリスペクトしつつ、これから先の未来におけるZOZOTOWNの成長のために、今考えられる最適なアーキテクチャを検討し、引き続きリプレイスを進めていきます。現在、アプリのAPIサーバのリプレイスや、基幹システムのリプレイスも進めていますので、今後またどこかで紹介できたらと思います。 最後になりますが、全8回にわたり、お読みいただきありがとうございました。読者のみなさんにとって、少しでも有益な情報になっていたらうれしいです。 本記事は、執行役 兼 CTOの瀬尾 直利、EC基盤開発本部 本部長の高橋 智也、ZOZOTOWN開発本部 ZOZOTOWN開発3部 フロントエンドリプレイスブロック ブロック長の新家 弘久、そして同 フロントエンドリプレイスブロックの森 泰樹によって執筆されました。 本記事の初出は、 Software Design 2024年12月号 連載「レガシーシステム攻略のプロセス」の最終回「フロントエンドエンジニアから見るZOZOTOWNリプレイスとまとめ・今後の展望」です。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com *1 : 前述の「テスト」項目で記載したカナリアリリースとは目的が異なるものです。