LLMを活用した商品検索タグ自動生成とRecall改善の取り組み(BigQuery × Gemini)

はじめに

こんにちは、10Xで検索推薦の機能・基盤の開発運用を担当している安達(id:kotaroooo0)です。 10Xでは小売チェーン向けECプラットフォームStailerにおいて、商品検索機能ではElasticsearchを利用しており、主にテキストマッチングによって検索を実現しています。

今回、LLMを活用して商品検索タグ(以下、タグ)を自動生成し、検索対象に加えることでRecallを改善しました。 本記事では、その取り組みの背景、具体的な施策、設計、評価、そして得られた成果についてご紹介します。

課題:情報不足による検索ヒット率の低さ

Stailerでは、商品データに含まれるテキスト情報が限定的であるために、ユーザーが検索しても意図する商品がヒットしないケースがありました。

例えば、ユーザーが「メイク落とし」と検索しても、商品名やカテゴリ名、商品説明文に「メイク落とし」という文言が含まれていないクレンジングオイルの商品はヒットしませんでした。 特に商品説明文自体が存在しない商品も多く、全体的に検索に利用できるテキスト情報が不足している状況でした。

この問題に対しては、小売事業者様が管理画面から手動で商品に検索キーワード(同義語や関連語)を紐付ける機能を提供していました。 しかし、対象となる商品数が膨大であることや、キーワード選定・入力の手間から、この機能は十分に活用されているとは言えず、手動運用によるカバーには限界がありました。

また、10X内部でもElasticsearchに類義語辞書を登録することで継続的にRecall向上に取り組んでいますが、こちらも手動でのメンテナンスが必要であり網羅性には限界がありました。

施策:LLMによる検索タグの自動生成と活用

これらの課題に対し、商品名とカテゴリ情報をインプットとして、LLMにタグを自動生成させるアプローチを採用しました。 生成したタグをElasticsearchの商品データに追加し、検索対象とすることで、ユーザーが入力するであろう多様な検索キーワードに対応し、Recallの向上を目指しました。

なぜタグ生成か?

ベクトル検索やシノニムの自動生成など他のアプローチも検討しましたが、まずは既存のテキスト検索の仕組みを大きく変更せず、低コストかつ迅速にRecallを改善する手段として、LLMによるタグ生成が有効と判断しました。

また、ネットスーパーの検索特性として、ユーザーは欲しい商品が比較的明確であり、商品名やカテゴリ名を表す短いキーワード(例: 「牛乳」, 「パン」, 「シャンプー」)での検索が多い傾向があります。 このような検索行動に対しては、関連キーワードをタグとして補強するアプローチが効果的ではないかと考えました。

設計

タグ生成からElasticsearchへの反映までの処理フローは、以下のようになっています。

タグ生成と紐付けの概要図

このフローは、大きく以下の2つの独立したプロセスで構成されています。

  • タグ生成パイプライン
    • 日次で実行
    • LLMを使ってJANコードごとにタグを生成し、その結果をFirestoreに保存する役割
  • Elasticsearchへのインデキシング
    • 差分更新であり在庫情報が更新されるタイミング(1日1回以上)で実行
    • 更新された在庫情報と、対応するJANコードのタグをマージし、Elasticsearchのインデックスを更新する役割

それぞれの実行タイミングが異なるため、生成されたタグが実際に検索結果に反映されるまでに最大1日のタイムラグが発生する構成となっています。

JANコード単位での生成と管理

Stailerは複数の小売事業者様がそれぞれ複数の店舗を運営しており、同じ商品(同一JANコード)でも在庫を持つ店舗ごとに個別のElasticsearchドキュメントが存在します。 全てのドキュメントに対してLLMでタグ生成を行うと、APIコストと処理時間が膨大になります。

そこで、JANコード単位でタグを生成・管理し、Elasticsearchへのインデックス時にJANコードをキーとして各ドキュメントにタグ情報を付与する方式を採用しました。 これにより、LLMのAPIコール数を商品数(JANコード数)に抑えることができました。

タグデータの更新について

更新頻度:バッチ更新 vs リアルタイム更新

No Option Pros Cons
1 バッチ更新 既存のMLパイプライン基盤を利用でき実装がシンプル 新商品が登録されてからタグが付与されるまでにタイムラグが発生する(最大1日)
2 リアルタイム更新 新商品登録後、即座にタグが付与される イベントドリブンな新しいアーキテクチャの構築・運用コストが必要

結論: バッチ更新を採用。タグは検索体験を補強するものであり、必須データではありません。 新商品へのタグ付与遅延による影響は限定的と判断し、実装・運用のシンプルさを優先しました。

更新方法:全件洗い替え vs 差分更新

No Option Pros Cons
1 全件洗い替え LLMモデルやプロンプト改善の恩恵を常に受けられる。常に最新の状態で上書きするため、差分管理ロジックが不要。 LLM APIコストの増大: 変更がない商品に対しても毎回APIコールが発生し、非効率。また、処理時間が長くなる。
2 差分更新 LLM APIコストの最適化: 新規商品や商品情報が更新されたもののみを対象にタグ生成を行う。また、処理時間も短くなる。 どの商品に対して再生成が必要かを判断する差分検出ロジックが必要になる

結論: 差分更新を採用。APIコスト、処理時間の抑制のため、差分検出ロジックの実装コストは許容できると判断しました。

タグ生成の品質とリスク

LLMが生成するタグの品質は100%正確ではありません。 不適切なタグが検索結果に悪影響を与えるリスクを考慮し、以下の設計にしました。

  • 手動でのタグ管理: 必要に応じて自動生成されたタグを確認し、追加・削除できる設計に。
  • フロントエンドへは非表示: 生成されたタグはあくまで検索ロジック内部で利用するものとし、商品詳細ページなどでユーザーに表示しない。
  • 検索スコアへの影響抑制: 検索結果のランキング精度を維持するため、タグの検索スコアへの重みは0に設定。これにより、タグによってヒットするようになった商品は検索結果の末尾に表示され、既存のランキングロジックへの影響を最小限に留めました。まずは「見つからない」を減らすことを最優先しました。

プロジェクトの進め方

1. PoC:タグ自動生成の実現可能性と品質検証

LLMによるタグ生成が実用に足るか、コスト・処理時間はどの程度かを見極めるため、以下の検証を行いました。

モデル、プロンプト

  • モデル候補: gemini-1.5-progemini-2.0-flash
  • 検証データ: 約1000件のJANコード(商品名、カテゴリ情報)
  • プロンプト: 商品名とカテゴリ名をインプットとし、複数のパターンを試行しました。
  • 評価:
    • 定性評価: 生成されたタグの関連性を目視で確認。
    • コスト試算: 各モデル・プロンプトでのトークン数を計測し、全商品(約15万JANコード)に適用した場合のAPIコストを試算。

今回のタスクは短いテキスト生成であり、gemini-2.0-flashで十分な品質のタグが生成でき、かつコストと処理速度の点で優位性があると判断し採用しました。 gemini-2.0-flashでは15万JANコードに対して、$6程度で想定以上に安価でした。

検証で十分な精度が期待できなければ別モデルも検証しようと思いましたが、gemini-2.0-flashがコストパフォーマンスが優れており今回は試しませんでした。

タグを生成する基盤

LLM APIを直接叩く方式も検討しましたが、既存のデータ処理基盤であるBigQueryとの親和性、運用負荷を考慮し、BigQuery MLのML.GENERATE_TEXT 関数の利用を第一候補としました。

cloud.google.com

  • 検証内容:
    • サンプルデータ(1000件)に対するSQLでのタグ生成処理の実行時間とコストを計測
    • 既存のMLパイプラインへの組み込みやすさを確認
    • 全データ(約15万JANコード)に適用した場合の処理時間を見積もり
      • 12時間程度と見積もれた。差分更新にするため、全データに対して生成するのは初回実行だけ。

結果として、BigQueryのML.GENERATE_TEXT は、既存のパイプラインへの統合も容易であると判断し、採用することにしました。

2. インデキシングの実装

タグ生成パイプライン(VertexAI Pipelinesで日次実行)

  1. 対象抽出: BigQueryで、前回の実行以降に新規追加または商品情報(商品名・カテゴリ)が更新されたJANコードを特定。
  2. タグ生成: 対象JANコードの商品名・カテゴリ情報を取得し、BigQueryのML.GENERATE_TEXTを用いてタグを生成。
  3. 結果保存: 生成されたタグを、JANコードをキーとしてFirestoreに保存。

indexing worker

  1. データ取得: Pub/Subから更新対象の在庫情報(JANコード含む)を取得。
  2. タグ取得: 対象JANコードをキーとしてFirestoreから対応するタグ情報を取得。
  3. データマージ: 在庫情報とタグ情報をマージし、Elasticsearchに投入するドキュメントを生成。
  4. インデキシング: Elasticsearchにデータをインデックス。

3. 商品検索ロジックの評価とプロンプトチューニング

生成されたタグを検索対象に含めた新しい検索ロジックを評価し、問題があればプロンプトやロジックを修正するサイクルを回しました。

評価

タグ追加によるRecall向上(ヒット件数増加、ゼロマッチ率減少)を期待し、評価を行いました。

  • 定性評価

    • 目的: 新ロジックで不適切な商品が検索結果に含まれていないか、意図通りに検索結果が改善されているかを確認。
    • 方法: デモ環境で、実際に様々なキーワードで検索し、新旧ロジックの結果を目視比較。
    • 主な確認キーワード:
      • 従来ゼロマッチだったキーワード
      • 検索結果数が少なかったキーワード
      • 検索ボリュームが多い主要なキーワード
  • 定量評価

    • ゼロマッチ改善率: 従来ゼロマッチだったキーワード群を新ロジックで再検索し、ヒット件数が0件でなくなる割合を計測。
    • ヒット件数変化: 主要キーワード群における平均ヒット件数の変化を計測。
    • nDCG: 既存の検索ログを用いて算出したnDCGスコアが、新ロジック導入によって悪化しないことを確認。タグによるヒットは結果の末尾に追加されるため、理論上nDCGへの悪影響はないと考えられたがガードレールとして確認。

プロンプトチューニング

デモをしていると、タグの品質にばらつきが見られ、いくつかの具体的な課題が見つかりました。 例えば、冷凍でない「うどん」に対して「冷凍うどん」という誤った温度帯を示すタグが付与されたり、商品の実態とは異なるサイズ(例: 「大容量」)やブランド名が生成されたりするケースがありました。

また、商品の「味・成分」(例: 「無添加」「グルテンフリー」)や「用途・対象」(例: 「赤ちゃん用」)に関するタグが誤って生成されることはリスクが高いと判断しました。 これらの情報が不正確だと、ユーザーに誤解を与え、購買体験や安全性を損なう恐れがあるためです。

このリスクを低減し、より安全で関連性の高いタグを生成するため、プロンプトに具体的な制約ルールを盛り込みました。 試行錯誤を経て、最終的に以下のプロンプトを作成しました。

Instacartのようなネットスーパーの商品検索精度改善を担当しています。
商品検索のRecallを向上させるために、次の商品に関連するキーワードを生成してください。
【商品情報】
商品名: {product_name}, カテゴリ名: {category_names},
【キーワード生成ルール】
1. 商品名やカテゴリ名に含まれる単語、または部分一致する単語は生成しない。
2. 次のような誤解を招く可能性のある属性を示すキーワードは生成しない。
- 温度帯: 冷凍, 冷蔵, チルド, 常温
- 味・成分: 無糖, 加糖, 低糖質, グルテンフリー, オーガニック, 無添加
- サイズ・量: 大容量, 業務用, 小分け, 一人前, ファミリーサイズ
- ブランド・地域: 北海道産, 沖縄産, 国産, 地域限定, 直輸入
- 用途・対象: 赤ちゃん用, 幼児向け, ペット用
3. 商品に関連性が高く、かつ一般的に検索される可能性のある単語を優先する。
4. 料理・用途・関連カテゴリを考慮したキーワードを生成する。
【出力ルール】
- 出力は生成されたキーワードのみで構成し、スペースなしのカンマ区切りで出力する。
- 生成するキーワードの数は、関連性が高い順に最大3つに制限する。

4. 検索ロジックの修正と本番リリース

評価とチューニングを経て、品質に問題がないと判断できた段階で、生成されたタグを検索対象フィールドに加えるロジック修正を行い、本番環境へリリースしました。

5. 本番リリース後の効果測定

今回は、開発期間やリソースの制約からABテストやインターリービングは実施せず、リリース前後の主要KPIを比較することで効果を測定しました。

結果

  • ゼロマッチレート: 15%減少
  • 平均ヒット件数 : 8%増加

ゼロマッチレートのモニタリング

おわりに

本記事では、商品情報の不足による検索Recallの低さという課題に対し、LLMを活用して検索タグを自動生成し、既存のElasticsearchベースの検索システムに組み込むことで改善を図った事例を紹介しました。 設計段階でのトレードオフの考慮や、LLM生成物の品質担保、評価方法などが、同様の課題に取り組む方々の参考になれば幸いです。

今回はLLM活用の第一歩として、まずは既存の仕組みと連携しながら検索のヒット率を改善しましたが、LLMにはお客様の買い物体験を大きく進化させる可能性があると考えています。

例えば、ネットスーパー特有の課題として、一度の買い物で多くの商品をカートに入れる手間が挙げられます。 RAGのような技術を取り入れれば、「今週の献立に必要な食材をまとめて提案し、ワンタップでカートに追加できる」といった、新しい買い物体験を提供できる可能性があります。 このようなユーザーの課題解決に繋がるLLMの活用についても、今後挑戦していきたいです。

技術的チャレンジへつながるピックパックのモジュール化 | お届けチーム取組紹介

はじめに

10X ソフトウェアエンジニアの鈴木です。 これからしばらく「お届けチーム取組紹介」と題して「イベント駆動アーキテクチャ」につながることを複数の記事に渡ってお伝えしていきます。 product.10x.co.jp

お届けチームでは、 「ピックパック」というお客様から承った内容を元に、商品を売り場や在庫置き場からとってきて(ピック)、箱詰め(パック)する業務領域(以下ピックパック)のシステムを疎結合化することで、技術的な課題を解決することを目指してきました。

社内では「モジュール化」という言葉を使用して取り組んできましたが、これは「ピックパック領域のコードをお客様へ対面するためのコードと疎結合にする取り組み」でした。本記事では、この取り組みの概要を紹介します。技術的な詳細については、後続の記事で解説します。

注文内容をピックしてパックして、配達へ受け渡す

チャレンジ前の構造

取り組み前のシステムでは、Orderクラス(as 1 Firestoreドキュメント)に、注文に関するあらゆる領域のロジックとデータ永続化処理が集中していました。具体的には、お客様の注文内容、ポイント処理、クーポン処理などのお客様との契約に関する情報から、ピックパック業務の状態、配達の状態など、注文処理全体に関わる情報が含まれていました。

このような設計は、単一のプロダクトで注文受付から配達完了までを管理できるという利点がある一方で、システムが密結合になり、機能の拡張や修正が困難になるという課題がありました。

たくさんの領域の関心ごとがOrderにいる図

主な課題は以下の3点です。

  • コード変更の複雑さ: 複数のチームがOrderクラスを共有していたため、コードを変更する際に他のチームとの調整が必要となり、開発のボトルネックとなっていました。
  • データ操作のロック競合: Orderドキュメントが複数の商品情報を含んでいたため、別々の商品に対する同時操作が頻繁に競合し、データ整合性の問題が発生していました。とくにピッキングの業務中では個別商品ごとに作業は並走するので問題が顕著に現れていました。
  • カスケード障害のリスク: ピックパック領域の障害が、お客様の注文明細表示など、直接関係のない機能にまで影響を及ぼすカスケード障害が発生しやすい状況でした。 例えば、ピックパック処理に問題が発生した場合、お客様が注文内容を確認できなくなるという問題が発生していました。お客様は、注文した商品が届くかどうかを知りたいだけなのに、スタッフの業務プロセスが原因で注文内容すら確認できなくなるという事態は避けるべきです。

とくにお届けチームのプロダクト改善はピックパックに関わることが現在多く、コードや構造改善の恩恵が得やすいということで改善の実施に踏み出しました。

構造の改善

これらの課題を解決するために、以下の取り組みを実施しました。

  • データモデルの分離: お客様対応に必要なデータとピックパック業務に必要なデータを、別々のFirestoreドキュメントに分割しました。これにより、各領域のデータを独立して管理できるようになりました。
  • コードの分離: データ操作を行うクラスも、お客様対応とピックパック業務で分離しました。これにより、各領域のコードの独立性が高まり、変更の影響範囲を局所化できるようになりました。

これらの取り組みに伴い、以下の2つのデータ連携が必要になりました。

  • お客様からの注文内容に基づいて、ピックパック担当者に指示を出す。
  • ピックパックの進捗状況をOrderドキュメントに反映する。

これらのデータ連携を、業務上の意味を持つ「イベント」としてモデル化し、イベント駆動型の非同期処理コンポーネントを導入することで実現しました。 また、ピックパックの進捗状況をリアルタイムに集計するために、イベントをトリガーとして集計処理を実行する仕組みも導入しました。

領域が非同期処理によって連携する(非常にシンプルにした図)

非同期処理のトリガーになるイベントを元に集計が起きている

これらの改善により、Orderとピックパックの間の影響関係がインターフェースを通じて明確になり、「実はXXXにも影響があった」という予期せぬ影響を減らすことができました。

ロック競合についてはピックの操作はPickingというある1注文の1商品ごとにあるデータへの操作になったので、最終的にOrderドキュメントへのデータ連携処理が残るものの、バックグラウンドで非同期に処理されるためスタッフの業務への影響はほとんどなくなりました。

カスケード障害のリスクも、データとコードの分離、および非同期処理の導入によって大幅に低減しました。ピックパック処理にバグがあった場合でも、ピックパック業務が開始されるまでの間に修正できれば、お客様やスタッフに影響が出るのを防ぐことができるようになりました。これは、改善前はピックパック処理のバグが注文受付自体を妨げる可能性があったことと比較すると、大きな改善です。


本記事では、ピックパック領域の疎結合化に関する取り組みの概要と、その効果について説明しました。技術的な詳細については、後続の記事で詳しく解説する予定です。

読者になるボタンを押すと後続の記事が公開されたときに通知を受けとれるのでおすすめです。

アプリを起動せずにアプリを開発して品質と生産性を上げる

先日、Flutter Tokyo #6 で同タイトルの発表をさせてもらいました。10分ほどの発表でしたが、割と良い反応をいただけたので、少し内容を補足してブログとしても公開します。

発表時のスライドは以下です。


前提

一般的に、モバイルアプリは自動テストしづらい箇所が多いと言われます。たしかに、画面から素朴に実装していくと、自動テストでは確認が難しくなりやすいです。そうなってしまうと、アプリを起動して手動で動作確認するしかなくなってしまいます。

一方で、設計やツールを適切に使えば、モバイルアプリであっても広範囲が自動テストで検証可能になります。手動での動作確認を完全になくすことはできませんが、手動テストへの依存度は下げられます。

手動テストへの依存度が下がると、検証時間の短縮、手戻りの抑制など、さまざまなメリットが得られます。また、手動テストにも良い影響を与え、手動テストでなければ確認できないことに集中できるようになります。

なぜテストしづらくなるのか

テストしづらさは、以下の2つに分解できます。

  • テストする条件の再現が難しい。
  • 結果の検証が難しい。

原因は色々と考えられますが、最もよくある原因はテスト対象の役割が大きすぎることだと思います。モバイルアプリではデータとUIの両方を制御する必要がありますが、両者の役割が混在すると状況はより厄介になります。また、役割が大きくなると記述するテストケースも網羅しづらくなり、抜け漏れが出てテストの価値が下がりやすくなります。

役割を混ぜないこと(特にデータの制御とUIの制御)、役割を大きくしないことを守れば、あとは場所ごとに適切な手段を使えば十分テスト可能となります。

例を使って解説

例としてGitHub APIでリポジトリ検索をして、結果を表示する画面を考えます。

この画面はそれほど複雑ではありませんが、それでも一気にすべてをテストするのは得策ではありません。役割の種類を混ぜないこと、役割を大きくしすぎないことを守りながらクラスの境界を設計し、それぞれをテストしていきます。

データとUIの関係を疎にする

数ある責務分割の中でもトップクラスで効果的なのは、データとUIの関係を疎にすることです。これを守ると、先に挙げた「テストしづらさ」の要因の1つである「テストする条件の再現が難しい」は解消しやすくなります。

依存関係の向きは常にUI → データなので、データとUIの関係を疎にするには、データ側のロジックの詳細がUI側に漏れないように設計します。今回の例ではデータ側はGitHubと通信を行い、レスポンスのステータスコードを確認し、JSONをパースし、データが想定通りか検証する、といった処理が必要ですが、この一連の内部処理の制御にはUIは関わらないようにします。

具体的な実装としては、データ側は以下のようなインターフェースを提供します。

UI側の視点で見れば、データ側に入力を渡せば結果が得られる状態になっています。一連の内部処理について立ち入ることはできず、結果だけを受け取るようになっています。

出力を適切にレイアウトに反映するには、内部処理の中で何が起きたか知る必要がある場合もあります。そういった場合に備えて、データ側では実行結果に適切な情報を含めるようにします。上記の例では、検索の失敗時にGitHubExceptionをスローすることになっていますが、この例外に適切な情報が含まれていれば、UI側は利用者と適切にコミュニケーションができます。

データとUIの関係が疎になると、UIの役割は「データに入力を渡して、出力をレイアウトに反映するだけ」というシンプルなものになります。これはコード上にもあらわれ、実際にwidgetの実装は以下のようになります。

なお、ここではRiverpodを使っており、前掲のデータ側のインターフェースを呼び出すproviderを以下のように定義しています。

ここでもう1つ重要なのは、UIが依存するデータ側の実装が差し替え可能になっているということです。Riverpodにはproviderを差し替えるoverrideという機能があり、データ側の出力を任意のものに変えられます。これによって、先に挙げた「テストしづらさ」の要因の1つである「テストする条件の再現が難しい」は解消されるという訳です。

役割がシンプルになったUIをテストする

UIの役割は「データに入力を渡して、出力をレイアウトに反映するだけ」というシンプルなものになりました。テストでもこの役割を検証します。

「テストしづらさ」の要因として「テストする条件の再現が難しい」「結果の検証が難しい」の2つを挙げていましたが、これまで説明していなかった「結果の検証が難しい」は、UIのテストにおいてはgolden testを使って解消します。

実際のテストケースに入る前に、テストする条件が再現されるwidgetを返す関数を準備します。テストする条件のデータの出力を渡して、providerをorverrideしてwidgetを返します。

ここまで来れば、あとは条件ごとにテストケースを書くだけです。以下のテストコードは、データ側がリポジトリのリストを正常に返した時に、その結果がUIに正しく反映されるか検証しています。

左側の画像はテストに使用するgolden fileです。右側のテストコードを書いて、flutter test--update-goldensオプションをつけて実行すれば生成されます。実装中はgolden fileが想定通りになるまで修正と再生成を繰り返します。一度完了したら、以降はテストのリファレンスとして使用します。

通信が失敗した場合のテストケースも同様に書けます。今度はresultFuture.error(GitHubException.connectionFailed()を渡して、通信が失敗した場合を再現しています。

このように「テストしづらさ」の「テストする条件の再現が難しい」と「結果の検証が難しい」をそれぞれ解決すれば、UIのテストも簡単に書くことができます。このような形で開発を進めていけば、アプリを起動して動作確認する必要性は自然と下がっていきます。

品質と生産性

視点を品質と生産性に移すと、以下のことが言えます。

  • 手動テストでは再現が難しい条件も含めて、あらゆる条件を簡単に再現できる。
  • 自動テストがリファクタリング耐性を持っているので、変更がしやすくなる。
  • 高速に反復実行できるため、開発中の修正サイクルを速く回せる。
  • 頻繁な全件実行が現実的なので、マイナーケースのデグレも早く発見できる。

あらゆる条件を簡単に再現できる

手動テストでは再現が難しい条件も、今回の形なら簡単に再現できます。

例えば、GitHubで障害が発生して500エラーを返した時にUIがどうなるかは、手動では再現が難しいです。しかし、今回の形であれば_createWidget()resultGitHubException.unsuccessfulResponse(statusCode: 500)を渡せばそれが再現できます。

あらゆる条件の再現が簡単になり、自動テストが実装されるということは、実行コストが大幅に圧縮され、その条件の検証が行われる機会が増えるということになります。それが、最終的には品質の底上げに繋がります。

リファクタリング耐性があるので変更がしやすい

UIのgolden testは、描画結果のスクリーンショットを使って検証しているため、widgetの内部実装には依存していません。そのため、widgetの内部実装を大幅に変えたとしても、テストコードを変える必要はなく、リファクタリング耐性があると言えます。

検証内容を変えない限りはテストコードを固定できるため、その分内部実装は柔軟に変更しやすくなり、結果として変更を早く終えられるようになります。

高速な反復実行による開発中の修正サイクルの高速化

今回のテストはFlutterのwidget testであり、これは非常に高速です。実装の仕方にもよりますが、今回のような形であれば10件くらいのテストケースも実行は1秒ほどで終わります。

また、Flutterにはhot reloadがありますが、これと比べてもメリットがあります。今回の形では、再現が難しい条件や、テキスト入力などのUIの操作が必要な条件などであっても、確認と修正のサイクルの速度はあまり影響を受けません。

頻繁な全件実行による品質の底上げ

テストの実行速度が速いため、手元でもCI上でも全件の実行が現実的になります。これにより、アプリの大通りでないケースについても、常に検証を行うことができます。

最終的に手動テストが不可欠であることに変わりはありません。なぜなら、今回の形のテストは結合した状態でテストしている訳ではないため、結合に起因した不具合や、使い勝手という観点の検証ができないためです。しかし、それでも自動テストは手動テストの負担を下げたり、稀にしかテストされないマイナーケースも含めた全体の品質の底上げに役立ちます。

例えば、レイアウト崩れを早期発見するgolden testがあれば、レイアウト崩れによる手戻りの頻度は大幅に抑えられ、開発担当者とテスト担当者が手戻り対応に割く時間を抑えることができます。

UI以外のテスト

今回はUIのテストを中心に説明しましたが、アプリを起動せずに開発を進めるにはデータ側のテストも同様に重要です。また、UIに関しても画面遷移のテストや、分析ログのテストなど、まだまだテストすべきものがあります。

これらについては、また別の機会に解説したいと思います。

まとめ

  • モバイルアプリ開発は手動テストに依存した部分が増えやすいと言われる。
  • しかし、役割をちゃんと分解すれば、ほとんどのことが自動テストできる。
  • それなりに頑張る必要はあるが、やる価値はある。

メンバー募集

10Xではソフトウェアエンジニアを募集しています(特にバックエンド)。

今回はモバイルアプリのテストの話を書きましたが、役割の分解のところはバックエンドでのテストにも通ずるものです。こういった考え方で開発をより上手くやっていきたいという方に是非来ていただきたいです。

興味があれば、ぜひ会社紹介をご覧ください!

open.talentio.com

お届けチームがイベント駆動アーキテクチャを採用した理由

ネットスーパーで注文された商品が効率よく確実にお客さまのもとに届くためには、店舗でのピッキングやパッキング、配送といった業務が必要となります。この業務を支えるStailer上のアプリケーション開発を担っているのが、お届けチームです。

10x.co.jp

お届けチームは昨年「イベント駆動アーキテクチャ」を導入する取り組みを行いました。イベント駆動アーキテクチャをどんな狙いで導入し、どんな成果が得られたのか。お届けチームの開発メンバーである鈴木さんに聞いてみました。

イベント駆動アーキテクチャ: イベント駆動アーキテクチャは、システム内で発生するイベントをトリガーにして処理を実行する設計パターン。コンポーネント同士が直接やり取りをせず、イベントを介して情報を共有するため、疎結合になり、スケーラビリティと柔軟性が高くなる。イベントはイベントバスやメッセージキューを通じて通知され、受け取ったコンポーネントが必要に応じて処理を行う。

密結合なシステムの課題

── まず最初に、どんな課題を解決するためにイベント駆動アーキテクチャを導入しようとしたのか、というところを聞いてみていいですか?

鈴木: はい。 元々あった課題として、Stailerはいろんな領域のデータだったり、コードだったりと、いろんなものが密結合になってたんですね。 密結合になってるがゆえに、ソースコードを触るときに、あっちにお伺いを立てて、こっちにお伺いを立てて、みたいな一つ触るのにも気にすることが多いという状況でした。そうした、開発の面での認知負荷が高いというところが1つ。

それに、障害リスクの観点がもう1つ課題としてありました。ネットスーパーをやってると、エンドユーザーが利用するECサイトと、店舗のスタッフの方が利用するシステム、2つのシステムを管理する必要があります。店舗向けのシステムで障害起きてしまうと、ECサイトまで注文できなくなるようなシステムが本当にいいシステムなのか?というと、そうではないと思うんですね。

実際、密結合がゆえにスタッフ向けのシステムの面がちょっと壊れたりすると、エンドユーザー向けのECサイトでも注文が受け付けられないといったような障害に繋がる、カスケード障害のリスクが当時はいろんなところに点在していました。そういうのを解決したくてイベント駆動を採用しようとしてた。 ってとこですかね。

Eventarcの活用

── そうしたモチベーションでイベント駆動を導入し始めたと思うんですが、導入するにあたってどんな部分が大変でしたか?

鈴木: そうですね。やはり、スタート地点でコードやデータがすごい密結合になっていたので、これをどうやって分解しようかな、というところですかね。イベントをどうモデリングするかっていうのは、頭を抱えましたし、モデルって、常に省みるものなので、今でもこれは正しいのかなぁ、どうなんだろうなぁっていうのは、悩み続けているし、大変だったところですかね。

一方でより技術的な面でいうと実はそんなに大変ではなかった、 とは思っていて。弊社ではFirestoreを中心的なアプリケーションのデータベースとして使ってるんですけど、それがEventarcっていうイベント駆動のためのサービスとうまく繋げることができたので、イベント駆動をシステム的に実現するっていうのは、思ったより大変ではなかったとこでした。

Stailerでは主にDB(Firestore)上の特定のデータの書き込みや更新をトリガーとし、Cloud Runを経由して期待されるコンポーネントの処理が実行される。

Eventarc: Google Cloudの内外のイベントを統合して処理できるサービス。Cloud StorageやCloud Pub/Sub、Firebaseなどからのイベントをトリガーにして、Cloud RunやCloud Functionsで処理を実行することができる。

意思を持って言葉を定義すること

── モデリングをちゃんとやろうとしたとき、使ってる言葉がブレてるとか、そもそも言葉の定義が決まってないみたいで苦労することがあるんじゃないかと思うんすけど、その辺で何か工夫されたこととかってありますか。

鈴木: まず、自分たちで決めないと、その言葉がないってことからスタートだなって思ってまして。

例えば会計とかクレジットカードみたいなドメインだと、その業界の中でいろんな言葉があったりするんですけど、僕らの作ってるピックパックってそもそも紙ベースで仕事がされてた頃からシステム化しましたっていう経緯がありまして。

紙のときにはそもそもやってなかった業務が増えてる。 増えた業務ってまだ名前がなかったり、ふわふわしてたり、名前があるけど、実は何かお届けの領域にとっては正しくない名前をしてるとか。

そういうのが多かったんで、自分たちでこれはこういう言葉を使うんだっていう強い意志を持ってことを決めるっていうことがすごい大事でした。そこで、そのユビキタス言語辞書を作ってみるっていうのはすごいやってよかったというか、これがなかったら厳しかったなっていうとこですよね。

お届けチームのユビキタス言語辞書の一部

── モデリングを見直したときって、既存のデータ構造を変えるみたいな必要があると思うんですけど、実際動くものがある状態でデータ構造とかモデルを変える大変さってありましたか?

鈴木: そうですね。 もちろんデータマイグレーションとかは大変なんですけど、幸いにもお届けチームが受け持つ領域って、一つのデータの生存期間というか、使われる期間が非常に短いんですね。

お客さまが注文を入れてから、手元に行くまでの間で扱われるデータなので、長くても2週間ぐらいで役目を終えるし、後からその生のデータを省みるってことはあんまりないんですよね。

データの生存期間が短いんで、並走期間はその2週間分を見てダブルライトして短い期間の移行期間を経て対応できたので、実はそんな大変じゃなかったんですよね。

イベント駆動アーキテクチャの恩恵

── お届けチームが持ってるコンポーネントと他のチームのコンポーネントが密結合だと、境界上から見えづらくてモデリングしづらいみたいなところとかもあったりするんじゃないかなと思うんすけど、その辺で苦労されたこととか、工夫されたことってありますか。

鈴木: そうですね。 ピックパックの話で言うと、幸い他のチームが運んできたものを最後扱うところなので、比較的自分たちの都合で決めるというか、あんまりお伺いを立てずに進められたかなとは思ってますね。

なので他チームとのインタラクションという観点だと、ここまではあんまり苦労はなかったです。ただ、チームの中で発見したり、作った言葉を他チームに浸透していく大変さを、これから味わっていくんだろうな、というのは予感しています。

── イベント駆動アーキテクチャを導入してみて、実際にどんな恩恵が得られましたか?

鈴木: 保守性の話にも繋がると思うんですけど、イベント駆動で開発をするとイベントを大事にするからモデリングをしっかりする、といったようにモデリングする動機付けになったという部分があります。

また、イベントを保存して扱うというやり方になったので、トラブルシューティングのときに信用できる事実が溜まってくってのはすごい良かったことですね。

以前からも業務の生産性を計測するために、いわゆるGoogleアナリティクスにログを送るみたいなことをしてました。 広い意味だとそれもログではあるんですけど、どうしてもデータが使いにくかったり、実際にシステムに反映されてる状態と、Googleアナリティクスにあるログは乖離し得るものなので、何かデータの不整合があるんですけどみたいなことがあったりしました。

イベント駆動を始めてイベントを保存するようになってから、それを基準に業務の生産性を図ることができるようになりました。システムの都合で変な不整合が起きないっていうのは、明確に前進したかなっていうところですね。

理想のモデリングを志すことに価値がある

── 正しく業務の内容を記録するされるっていう副産物も得ることができたってことですね。ありがとうございます。 最後に何か今、この話を聞いて、イベント駆動を自分をやってみたいなってなった人へのアドバイスとかってありますか。

鈴木: いろいろあるけど、やっぱりイベントを通してモデリングする業務に着目することですね。この業務の中ではどういうことが起きるんだろうっていうのに着目して作っていくことが何よりも大事かなって思ってます。

あとはイベント駆動なので非同期処理が入ってくると、非同期であるが故に技術的に難しいことがたくさん出てくる。 そこは失敗しながら、学んでいくしかないのかなと思ってます。

それから、もし仮にですけど試してみたが、やはりイベント駆動はやめておこう、という結果になっても、イベント駆動を志してモデリングした経験は残ると思うんですよね。イベント駆動アーキテクチャを最終的に採用しなくても、今後の自分のモデリングの糧にはなるはずなので、ぜひやってみるといいんじゃないかなと思います。

モデリングを大事にしてやってみると良い、というのは明確にアドバイスの一つかな、と思います。

── なるほど。イベント駆動が導入されたこと自体より、業務があるべき姿にモデリングされていることこそが一番良い成果、というような。

鈴木: そうですね。志すといいますか…あるべき姿、完璧になるってことはやっぱ難しいし、出来ないかもしれないんですが、でもそれを目指す。 目指して、頭の中に理想がある状態を作る。足元の現状を考える際にそうした理想とのギャップを考えることができると良くて、それをさらにシステムに反映できるとより良いな、と思ってます。

おわりに

10Xではより良い設計、より良いモデリングによってプロダクトや事業を成長させたい、というソフトウェアエンジニアを募集しています。 この記事を読んで、自分も理想のモデリングを志したいと、と感じた方はぜひ下記をご覧ください。 open.talentio.com

Devin AIの商品データパイプラインへの適用

はじめに

10X ソフトウェアエンジニアの野々村です。元々アプリケーション開発者としてサーバーサイドを中心に仕事をしてきましたが、最近は協業する小売事業者様から受領したデータをネットスーパーに掲載できる状態へ加工する商品データパイプラインの開発に主に携わっています。

10x.co.jp

10XでもDevin AIを利用し始めて1ヶ月程度が経ったので、適用事例と展望について紹介します。

前提

Devin AI とは

Cognition AI社によって提供されるAIエージェントSaaSです。2024年12月に一般利用が開始され、各所で導入事例が紹介されています。

devin.ai

Devin AIの特性として、以下のようなタスクが得意とされています。

  • 実装の検証が容易である
  • 参考実装にアクセス可能である

Devin AIと適用領域の特性のマッチング

10Xでは私が開発に携わる商品データパイプライン領域においてDevin AIの特性がマッチするのではという仮説があり、導入検証を行いました。

弊社の商品データパイプラインは大きく、「小売事業者様から受領したデータを計算処理に渡すための共通フォーマットにクレンジング・結合・絞り込み・整形を行う箇所」と「検証されたデータセットを元に共通の計算処理を行う箇所」の2層で構成されています。

前者の小売事業者様に近いレイヤーは、小売事業者様ごとに個別のパイプラインが実装されています。一方で完全なワンオフではなく、開発者の認知負荷を削減するため、一定のルールに基づいた共通構造が実装されています。

商品データパイプラインのイメージ図

そのため、「ある小売事業者様向けのパイプラインで実装した内容を他のパイプラインに水平展開する」ようなタスクが頻繁に発生します。このようなタスクは以下のような点でDevin AIに依頼しやすいタスク群でした。

  • 事前定義されたスキーマによって型制約を与えつつ実装が進められ、自動化された単体テストや結合テストで回帰試験が可能である → 作業中および作業完了後の検証容易性を担保できる
  • あるパイプラインへの実装を参考実装として渡してDevin AIに指示を出せる → 参考実装のアクセス容易性を担保できる

効果測定

導入による影響と費用対効果

商品データチームではスクラムを採用して1週間毎にSprintを回しています。4週間の消化StoryPointの比率からDevin AIを利用して対応したStoryPointを観測すると、概ね20-30%程度の消化StoryPointへの貢献が見て取れました。

商品データチームはEMを含め4人で運営しているため、20-30%は単純計算でエンジニアを1人追加した程度のインパクトと言えます。

一方で費用についてですが、Devin AIの料金はACUと呼ばれる計算量の単位で決まります。

devin.ai

記述時点(2025/3/17)では$500/250ACUsが月間の基本料金であり、以降は$2/ACUの従量課金です。

検証期間中利用したACUは400ACUs程度でした。金額換算では$800程度になります。¥150/$で換算すると¥120,000程度となります。

開発ツールへの出費としては安くありませんが、開発生産性へ与える影響との比較で言えば破格と評価しています。

導入時の工夫

使い始めるためのドキュメントを整備する

AIエージェントを利用した開発は開発者の受け入れコストが小さくありません。自分の手元で完結するコーディングとは異なるマインドセット、ワークフローを要求しますし、そもそも仕組みの理解やサービスの仕様について理解しておく必要もあります。

まずは使ってもらわなければ妥当な検証が行えないため、社内でのプラクティスをまとめた手引を作成し、少しでも初回利用のハードルを下げてもらえるようにしました。

実際に作成した手引の目次(一部)

最小権限を付与する

Devinは高い自由度を持ちます。ミニマムでも対象GitHub Repositoryへのwrite権限を持つため破壊的な変更を実行できますし、例えばインターネットアクセス環境とブラウザを持つため、認証情報を持てば多くの環境にログインして作業することができます。しかしセキュリティ的に、あるいは本番オペレーターとして信頼できる水準では必ずしもないという事例も共有されています。

note.com

note.com

一方でDevinへのタスク委譲を考えると一定の権限付与は避けられません。例えば商品データパイプラインにおいては、BigQueryへのwriteができないと修正後のパイプラインにおいてスキーマの一貫性が保たれているかの検証ができません。

このようなケースにおいて、Google CloudのProjectレベルの権限を付与するのではなく、データセット単位の権限付与を行うことで意図せぬ開発環境への影響が起こらないようにしました。具体的にはDevinが作成するデータセットは所定のprefixを設定する運用とし、Google CloudのIAM Conditionsを用いて特定データセットへのみwrite権限を持つように定義しました。

cloud.google.com

10XではIAMをTerraform管理しているため google_project_iam_member resourceを用いて以下のように実現しています。

registry.terraform.io

resource "google_project_iam_member" "${resourceName}" {
  project = "${projectId}"
  role    = "roles/bigquery.jobUser" # クエリ発行のために最低限jobUserが必要
  member  = "serviceAccount:{devin's ServiceAccount}"
}

resource "google_project_iam_member" "${resourceName}" {
  project = ${projectId}
  role    = "roles/bigquery.dataEditor"
  member  = "serviceAccount:${devin's serviceAccount}
  condition {
    title       = "only_owned_dataset"
    description = "商品データパイプライン用のDevinは自分用のdatasetのみ編集可能"
    expression  = "resource.name.startsWith(\"projects/${projectId}/datasets/dbt_devin_sandbox_\")"
  }
}

またDevin AIはSlack Appを用いたSlack Integration機能を持ちますが、このAppは参加したチャンネルの全メッセージの読み取り権限を持つため、所定のDevin連携用チャンネル以外では参加させない運用とし、予期せぬ情報漏えいを防ぐようにしました。

DevinによるPull Requestのレビュールールをチームで決定する

Devinが作成したPull RequestはGitHub上ではbot userが作成したものとなります。商品データチームの所管Repositoryでは、CodeOwnerによる1 approvalでマージ可能な運用としていたため、Devinが作成したPull Requestを依頼者がapproveすることで実質的なセルフレビューでのマージが可能になってしまいます。

これに対し我々のチームでは以下のような運用を決めて運用しています。

  • 原則、依頼者 + 1名のapprovalでマージ可能とする
  • 本番影響のないドキュメンテーション、テストコードの修正に限り依頼者のレビューでマージして良い

ドキュメンテーションの補填や欠けているテストの充足などもDevin AIに期待する役割に含まれるため、それらの速度を落とさず、一方で安全に本番運用を進めるために上記のようなルール設定としました。

Knowledgeは定期的にリポジトリにdumpさせる

Devin AIでは作業に必要な知識・コンテキストを Knowledge という形で登録することができます。

docs.devin.ai

作業毎にDevin AI側から更新を提案してくれるなど非常に便利な機能です。一方でチームのスタンスとして、今後Devin以外のAIエージェントやツールへの転換も十分あり得ると考えています。そのため、Devinに対し定期的に Knowledge をまとめたものをドキュメントにまとめてRepositoryに追記させ、重複するようになったKnowledgeは削除するような運用を取っています。

これにより、ロックインを避ける形でAI ReadableなRepositoryへの磨き込みを進めています。

今後の課題

適用範囲の拡大

今回の検証期間では商品データチームが主要なユーザであり、適用範囲はdbt, GitHub Actions等限定的でした。現在、terraformにて管理されるinfrastructure resourceやDartで実装されたアプリケーションコードへの適用を進めています。

開発チームのパフォーマンスへ与える影響については引き続き測定が必要ですが、特にDartのような静的型付けをもつ言語においては、SQLを主体とするdbtより直接に、高い表現力で値への要求を表現できるため、精度の向上が期待できるのではと考えています。

一方で技術的な課題として認証情報の使い分けがあります。DevinにSecret管理の機能はあるもののグローバルに利用可能なため、チームごとに最小権限で運用しようとすると一工夫が必要です。

docs.devin.ai

現状はSnapshotとして認証情報を保持した状態を保存し、作業指示する際に指定するような運用を想定していますが、改善の余地があります。

レビュー運用の仕組み化

上述の通り、現状のマージルールは運用によってカバーにしていますが、GitHub Actionsによる仕組み化を進めています。具体的には、Commit author および co-author*1 によるapproveしかない場合はfailするようなGitHub Actionsを実装し、GitHubのRuleSetにおいてrequiredとすることで、安全に運用できる状態を作ろうとしています。

導入しての所感

チームの生産性に与える影響はスケールアウト的

Devinを利用しての作業は概ね作業依頼と成果物のレビューというサイクルで成り立ちます。

Devinを使い倒すのであれば、例えばDevinに対して3件の作業を並列に依頼し、これを順次レビューしていくという方法も可能でしょう。一方でDevinに渡せるのは比較的ゴールの明らかな、複雑度の小さいタスクがメインとなるため、このようなタスクばかりを大量に消化するという場面も限定的です。

実際はDevinに定型的なタスクを依頼している裏で人間が複雑度の高いタスクを請け負う(あるいはDevinに渡すための最初の参考実装を作る)という働き方が最もうまく回るように感じました。

この場合、複雑なタスクと並行に簡単なタスクを進められるため、開発者の能力を高めるというよりは、既存の開発者と並列に作業を進めてくれるメンバーが追加されたような捉え方のほうが近いと考えます。

従って、今より複雑な課題が解けるというよりは、今までは後回しになりがちだった事にも適切に取り組めるようになるという工数制約の解消を期待する場合に、うまくフィットしそうです。

投機的な依頼という付き合い方

今のところDevinが作成する成果物は、常に100点を取ってくれるわけではありません。細かいところで意図と違う事をしてしまったり(ベタ移植して欲しいところを手直ししてしまったり、必要なコメントを消してしまったり…)、そもそも作業方針が期待と異なることも間々あります。

Knowledgeという形式で必要な知識を伝授したり、RepositoryにAIエージェント向けのヒントを拡充することである程度は精度を高められますが、それでも本番環境にリリース可能な成果物を常に生み出せるようになるまでにはしばらく時間がかかりそうです。

それまでの付き合い方として、投機的な依頼という付き合い方をチーム内には推奨しています。

依頼を投げてみて、人が作業すると4hかかるはずだった作業がうまくすると5-10minで完結する。もし細かいところが期待と異なっても、それを手直しすれば30minで対応できることがあります。

AIエージェントに確信を持って依頼できるタスクと、複雑度が高すぎて振りづらいタスクの中間に、できるかも知れないが完璧にはやれないかもしれないタスクが無数に存在します。これらを投機的に依頼してしまうことで、期待値としてのパフォーマンスは大きく伸ばしうると感じました。

We are hiring!

この記事では10Xの商品データチームにおけるDevin AIの導入について事例を紹介しました。

開発ツールとして考えると必要な費用が小さいツールではありませんが、開発生産性への影響は非常に大きいと捉えています。

生成AIの発展に伴いエンジニアリングの手法は大きな変革期を迎えつつありますが、この波を乗りこなしてともに10Xを作るメンバーを募集しています。

興味がおありのところがあれば、お気軽にカジュアル面談からお声がけください。

open.talentio.com

*1:デフォルトの設定ではDevinの積むcommitには依頼者がco-authorとして設定されます

アーキテクチャの変更をどうやって完遂するか

既存のアーキテクチャの問題が見えると、アーキテクチャを変更して解決すると思います。

それ自体は素晴らしいことなのですが、変更が全体に浸透し切らず古いアーキテクチャと新しいアーキテクチャが混在したままになってしまうと、状況はさらに悪化します。そのため、アーキテクチャを変更する時には、「どうやって完遂するか」もセットで考えるべきでしょう。

10Xの現状は?

混在しています。

自然と移行を完遂できる日は来なかったので、完遂する努力をしています。

完遂するための取り組み

アーキテクチャの限界を漸進的に押し上げる取り組み で紹介した通り、10Xでは 以下の4ステップのサイクルを回してアーキテクチャを改善しようとしています。

  • 欲しい特性を狙ってアーキテクチャ決定を積み重ねる
  • ADRでアーキテクチャ決定の意図を明確にする
  • linterでADRに辿り着けるようにする
  • ADRへの違反状況を可視化して漸進的に適応度を上げる

完遂に向かう話は4番目で触れているのですが、実際に移行を完遂するには非適合箇所がわかるだけではなく、適合状況の俯瞰的な現在地がわかる必要があると感じました。

そこで、以下のようなイメージの可視化を行いました。

横軸は日付で、縦軸は非適合の数です。アーキテクチャ決定ごとに色分けしています。

可視化にあたっては、以下の2点を把握できることを重視しました。

  • アーキテクチャ決定とコードの乖離は過去と比べて増えているのか。
  • コードとの乖離が多いアーキテクチャ決定はどれか。

アーキテクチャ決定とコードの乖離は過去と比べて増えているかどうかが把握できると、乖離を解消する動きに割く力が今くらいで十分なのか、それとも不足しているのか、理解できます。また、乖離の増減に加えて総量も把握できると、今が新しいアーキテクチャ決定を加えても良い時なのかどうかも判断できます。

コードとの乖離が多いアーキテクチャ決定を把握できると、何の乖離の解消に力を割けば状況が良くなるのか理解できます。乖離が生じている理由は様々で、単に移行の腕力不足の場合もあれば、アーキテクチャ決定の内容自体に移行を鈍らせる要因がある場合もあります。いずれにせよ、何を見るべきかのヒントにはなります。

可視化の仕組み

10Xではアーキテクチャ決定はADR化し、ADRへの非適合箇所はlintで検出できるようにしています。新しいADRの導入時には非適合箇所がたくさん出るので、当該箇所ではlintを無効にするのですが、この無効にするコード(Dartの場合は // ignore: <rule>)を可視化の材料にしています。

以下のようなスクリプトで日ごとの非適合箇所を抽出し、データを作成しています。

date=<YYYY-MM-DD>
sha=$(git log main --merges --until="${date}$ +0900" -1 --pretty=format:"%H")
git grep -rE "// (ignore|ignore_for_file): .+" "${sha}"

デイリーのデータ作成はGitHub Actionsで行い、BigQueryにロードし、Looker Studioでグラフ化しています。

やってみた手応え

実際にアーキテクチャ決定への適合状況を時系列で可視化してみて、今までよりも良い判断ができそうだと感じました。今自分が主に開発しているリポジトリでは「今はアーキテクチャ変更よりも既存のコードを適合させることに注力する」と判断していますが、今回の可視化は判断の拠り所となる事実であり、この判断を変える条件を考える材料にもなりました。

今後もより良い判断をするための仕組みづくりをしていきたいと思います。

メンバー募集

10Xでは技術的判断をより良くしていきたいソフトウェアエンジニアを募集しています。

興味を持っていただけた方は、ぜひ会社紹介をご覧ください!

open.talentio.com

しなやかなデータ連携に向けたdatacontract-cliへの貢献について紹介します

データ基盤チームに所属しているデータエンジニアの吉田(id:syou6162)です。10X社内のデータマネジメントの仕事をしています。

最近、チーム内外でData Contractを取り扱う機会が増えています。本エントリでは、Data Contractの実践にあたり利用しているdatacontract-cliというOSSをなぜ選定したのか、業務で利用するにあたり不足していた機能にpull requestを送って貢献した話を紹介します。

Data Contractの必要性: しなやかなデータ連携を実現したい

10Xのこれまでのデータ基盤は

  • プロダクトから出力されるデータをBigQuery上に取り込んだ上で
  • ディメンショナルモデリングなど分析に使いやすいデータに加工し
  • その上でTableauなどのBIツールで分析結果をダッシュボードで可視化する

というよくある形式でした(この役割はもちろん現在も担っています)。しかし、プロダクトの高度化や事業の進展に伴ない、以下のようなユースケースも登場してきました。

  • BigQueryで集計したデータをプロダクト側の管理画面に表示したい
    • 単純に集計すればよい、というわけではなく、DWHやBIツールなどで整備されているロジックと共通化された集計結果を出したい
    • プロダクト側の開発チームとしては、集計された結果のロジックの詳細には踏み込まず、表示や可視化の部分に集中したい
  • データプロダクト間でデータのやり取りが発生するため、その仕様をプログラムが扱いやすい形でやり取りしたい
    • 手動で仕様をやり取りする形式(例: スプレッドシートやnotionで仕様の管理)はやめたい。実装と乖離してしまうことが経験的にも分かっている
    • データがある状態にたまたまなっているのか、生成側の制約として担保されているのかをスキーマの制約として記述したい
    • コードから仕様の自動生成や仕様からコードの自動生成(例: dbtのsourceの自動生成、Protocol Bufferの自動生成)を行なえるようにして、チーム間のコミュニケーションを疎にできるようにしたい

データを中心としたこういった複数チーム間でのデータの仕様(スキーマやその制約)を担保しつつ、機械からread / writeが可能で、開発者同士のコミュニケーションはなるべく疎にするようなデータ連携を本エントリでは「しなやか」と定義します。

Data Contractの導入に向けたより詳しい背景については、以下の発表資料にまとめていますので、そちらを参照してください。

datacontract-cliを通じて、しなやかなデータ連携を実現したい

Data Contractの仕様を考える際、10X独自の仕様を自分たちで考えることもできますが、継続的なメンテナンスを考えると、世の中の標準に合わせるのがよいと考えました。Data Contractの仕様としてはData Contract SpecificationOpen Data Contract Standard (ODCS)がありますが、10Xでは前者のData Contract Specificationを選択しました。理由としては

  • 取り扱える情報にそれほど大きな差異がない
  • 仕様だけでなく、周辺ツールであるdatacontract-cliが強力であると感じた
  • datacontract-cliを通じて、 Data Contract SpecificationからOSDCへの変換が比較的容易であり、後から乗り換えることが可能

が挙げられます。datacontract-cliは様々な機能を持っているため、詳しくは本家の情報を参照して欲しいですが、10Xで特に魅力に感じた点は

  • 様々な種類のデータ仕様書からのdatacontract-speficiationへの変換
  • datacontract-speficiationから様々な種類のデータ仕様書への変換

が挙げられます。こうした変換は一個一個は大したものではないため、自作する選択肢もありえます。直近でありえる変換{元, 先}としては

  • dbtのsourceやmodel
  • protobufやjsonschema
  • great-expectationsなどのvalidatorコード

などがあり、これら全てを自分たちで実装 / 継続的にメンテナンスするのは骨が折れます。datacontract-cliを利用することで、事業ドメインに深く関わる時間をより確保できる可能性が高いと考え、datacontract-cliを採用することにしました。

datacontract-cliに取り込まれた修正

これまで説明したように魅力的な機能を持つdatacontract-cliですが、10Xの業務利用を考えると不足している点も多かったです。ここでは、業務で利用可能にするためにdatacontract-cliにpull requestを送って取り込まれた修正について紹介します。

Data Contractとdbtの相互変換の際の型修正

最近Snowflakeの利用が増えているというのはOSSの世界でも感じることが多いですが、これはdatacontract-cliにおいても同じでした。data contract側で持っている型とdbt側で持つ型を変換する際、Snowflakeしか考慮されておらず、そもそもBigQueryが対象のdbtモデルではdatacontract-cliが動かない、という状況でした。そこで、BigQueryをはじめ、他のDWHにおいても動かしやすい形にリファクタリングし、Snowflake以外が対象でも動くように修正をしました。

スキーマの制約が反映されるように修正

Data Contractとdbtで仕様を変換する際、当初はスキーマの制約に関する情報の大半が抜け落ちてしまう状況でした。具体的には当初は

  • テーブル名、テーブルのdescription
  • カラム名、カラムのdescription、カラムの型

といった本当に必要最小限の情報しか変換後は残っていませんでした。チームをまたいでデータを取り扱う場合、以下のようなよりリッチなスキーマに関する情報が必要なことが多いです。

  • テーブルの主キーはどれか
  • 該当カラムは値が入っていることが必須かどうか(dbtのnot_null)
  • 該当カラムの値は一意であるか(dbtのunique)
  • 該当カラムはどのカラムの外部キーになっているか(dbtのrelationships)

10Xでもこういった情報はこういった情報はまさに必要としたため、datacontract-cliに対応するpull requestを少しずつ送り、その結果全て取り込まれました。

その他の細かい修正

これまで紹介した修正以外にも、datacontract-cliの使用感に関わる細かい修正も多数pull requestを送り、取り込んでもらいました。

Pull Requestが取り込まれたことでできるようになったこと

これまでに紹介した修正により、datacontract-cliを使って以下のData Contractの運用ができるようになりました。

  • データ基盤チームが管理するBigQuery上のデータの仕様をData Contractのyamlとして渡す
    • dartやprotobufの自動生成などに利用を想定
  • データメッシュ的な文脈でデータアプリケーションBが必要とするデータアプリケーションAの仕様をData Contractを通じてやり取りする
    • データアプリケーションAとBはそれぞれdbtで動いており、dbtのmodel定義 => Data Contract => dbtのsourceという形でdatacontract-cliを使って変換

Data Contractとの相互変換はGitHub Actions上で行ない、成果物であるyamlファイルはGitHub Releasesとして保存するようにしています。また、主キーや外部キーといったスキーマ上の制約も相互変換内で欠落することなく保持できるようになったため、しなやなかデータ連携に向けてのスタートを切ることができたかなと思っています。

今後の予定

現状の10Xの業務で必要なレベルのものは上記でひとまず揃ったのですが、まだまだ整備したい点がdatacontract-cliにはあると感じているので、いくつか紹介します。

インストール時に必要な依存ライブラリの整備

datacontract-cliはData Contractと多様な形式への相互変換ができることが魅力の一つですが、多様な形式をサポートするため、インストールに必要なライブラリが非常に多くなっています。ユーザーサイドが使う予定のない形式のための依存ライブラリもインストールされてしまうため、pip installなどの複雑性が上がってしまっています。実際、dbtやdbt-osmosisなど他のエコシステムと同居させる際に、依存ライブラリのバージョンの噛み合いが悪く、dbtが最新バージョンになかなか上げられない、といった弊害も出てきています。

そこで、ユースケース毎にoptional-dependencyとして切り出す作業を少しずつ進めています。

追加的なスキーマの制約に関する仕様のやり取りをできるようにする

本エントリでData Contractと相互変換をする際にリッチなスキーマ情報が落ちないようにする、という話を書きましたが、Data Contractの仕様ではよりリッチな情報を保持できます。例えば

  • あるカラムが取り得るenumの値の種類を記述
    • これらの値のどれかしか入らない
    • dbtのaccepted_values
  • あるカラムが取り得る値の範囲を記述
    • dbtのdbt_utils.accepted_range

といったものがあります。そういったものも相互変換の際に抜け落ちないようにする対応をいつかやりたいなと思っています。

まとめ

本エントリでは

  • 10XでData Contractが必要になった背景 / 課題
  • しなやかなデータ連携を実現するため、datacontract-cliを選定した理由
  • 業務で活用できるレベルまでdatacontract-cliにpull requestを送って貢献した話

を書きました。datacontract-cliは2024年のAdvent Calendarでも紹介している記事が複数上がっており、盛り上がりを見せています。

本エントリで紹介した内容やより踏み込んだ話題を上記のエントリの著者の方々とパネルディスカッションで議論する場を来月(?)主催しますので、ご興味ある方は是非ご参加ください!

参考: