Rails + SQL Server環境でハマったBooleanの罠

こんにちは、WEBエンジニアのChoi(チェ)です。 BUYMAの購入者向け機能を開発するチームで、主にSEO改善の業務を担当しています。

この記事はEnigmo Advent Calendar 2025の23日目の記事です。

Railsを使用する際は一般的にMySQLPostgreSQLが使われますが、BUYMAでは用途に応じてSQL Serverも使用しています。

最初は「どのSQLも大差ないだろう」と思っていましたが、運用を開始するとRails + SQL Server特有のトラブルに遭遇しました。

今回はその中でも、

エラーは一切出ないのに、結果だけが返ってこない

という、かなり気づきにくかったケースをご紹介します。


boolean処理をめぐる誤解

ある APIで、次のようにリクエストパラメータを条件に使っていました。

User.where(active: params[:active])

エラーは発生せず、一見すると問題なさそうに見えました。

しかし実際には、エラーは発生しないものの、条件に一致するデータがまったく返ってこないという現象が起きていました。

SQLログを確認すると、発行されていたのは次のようなSQLです。

WHERE active = 'true'

この時点ではまだ、

「文字列になっているのが問題なんだ」

という程度の認識でした。

しかし「文字列になっていたら、trueに暗黙的に変換されるのではないのか?」という疑問も浮かびました。

RubySQLの違い

Rubyでは、次のようなコードが成立します。

if 'false'
  # 実行される
end

Rubyの条件分岐では、falsenil以外はすべてtruthyとして扱われるため、'false'という文字列も「真」として評価されます。

しかしこれは Rubyレベルでの話です。

ActiveRecordが生成するSQLでは、値は型変換されることなく、そのままバインドされます。

テーブル定義を見て、ようやく原因に気づく

改めてテーブル定義を確認してみると、activeカラムはbooleanではありませんでした。

  • カラム型:CHAR(1)
  • 想定値:'1'(有効) / '0'(無効)

つまりこのコードは、

HTTP パラメータとして受け取った 文字列をそのままSQLの条件に渡していた

という状態だったのです。

SQL Server側では、'true'booleanとして解釈されません。

あくまで 文字列同士の比較として評価されます。

その結果、

  • エラーは出ない

  • 条件にも一致しない

  • 常に0件が返る

という、分かりづらい不具合になっていました。

MySQLPostgreSQLであれば、どうなっていたか

ここで気になったのが、 「もしMySQLPostgreSQLだったら、同じ問題は起きていたのか?」 という点でした。

PostgreSQLの場合

PostgreSQLには明確なboolean型があります。

WHERE active = 'true'

のような条件でも、'true'booleantrueとして解釈します。

そのため、今回のケースでは 意図した通りにデータが返ってきていた可能性が高いです。

結果として、問題に気づかないまま運用が続いていたかもしれません。

MySQLの場合

MySQLでは、booleanは実体としてはTINYINT(1)です。

'true''false'といった文字列は、暗黙的に数値へ変換され、結果が返ることもあります。

ただしこの挙動は、明確な仕様というより暗黙の型変換に依存したものです。

なぜ SQL Server環境で表面化されたのか

この問題自体は、SQL Server固有の文法エラーではありません。

しかし、SQL Serverを採用しているサービスでは

  • レガシーなテーブルでは、有効/無効フラグをCHAR型で管理しているケースが今も存在する

という背景から、Rails + SQL Serverの組み合わせで特に踏みやすい落とし穴だと感じました。

この経験から学んだこと

このトラブルをきっかけに、次の点を強く意識するようになりました。

  • DBのカラム型は「現在のRails の常識」と一致するとは限らない
  • パラメータをそのまま条件に渡さず、ドメイン上の値('1' / '0' など)に変換してから使う
  • 結果が返らない場合は、生成されたSQLを必ず確認する

Rails側で正しく書けていても、DB側の前提が異なるだけで、意図しない挙動にハマることがあると学びました。


明日の記事の担当はWebエンジニアのレミーさんです。お楽しみに。

Rails 8のSolid Queue:Sidekiqに代わる新しい非同期処理システム

こんにちは!Webアプリケーションエンジニアのレミーです!

この記事はEnigmo Advent Calendar 2025の21日目の記事です。

Rails 8がリリースされてから、バックグラウンドジョブシステムである Solid Queue に興味を持ち、調べてみました。

バックグラウンドジョブは、Ruby on Railsアプリケーションに重要な部分です。メール送信、画像処理、データ同期、キャッシュ更新、CSVファイルのエクスポートなど、これらはすべてアプリケーションの高速化とスムーズな動作を維持するために非同期で実行すべきタスクです。

長年、Railsのバックグラウンドジョブにおいて「Sidekiq + Redis」はほぼ基準とされてきました。しかし、Rails 8からは、Railsは公式にSolid Queueを導入しました。これはRedisを必要とせず、補助的なサーバーも不要な、ネイティブなキューシステムです。

この記事では、Solid Queueとは何か、その仕組み、どうしてRails 8以上のプロジェクトでSolid Queueを使用すべきかについて解説します。また、Sidekiqとの比較も行います。

Solid Queueとは?

Solid Queueは、データベースをジョブキューとして使用するバックグラウンドジョブシステムであり、Rails Solid Suite(Solid Cache, Solid Queue, Solid Cableを含む)の一部として開発されました。

Redisを使用するSidekiqとは異なり、Solid Queueはジョブをデータベースのテーブルに保存し、ワーカープロセスがそのジョブを読み取って実行します。

つまり:

  • Redisが不要
  • Sidekiqのインストールが不要
  • 補助サーバーのコストがかからない
  • ActiveJobと深く統合されている
  • インストールが非常に簡単

これは、Railsをシンプルにするために生まれました。特にスタートアップ、小規模〜中規模のプロジェクト、またはコストを抑える必要がある環境に最適です。

仕組みとデータベース構造

Solid Queueは単一のシンプルなテーブルだけではなく、ジョブのライフサイクルを管理し、安全性とパフォーマンスを確保するために複数のテーブルを使用します。

重要なテーブルは以下の通りです:

  • solid_queue_jobs: ジョブのメタデータ(クラス名、引数、キュー名、優先度、遅延ジョブの場合はscheduled_at、ジョブIDなど)を保存します。
  • solid_queue_ready_executions: 「実行準備完了」となったジョブを含みます。つまり、エンキューされたジョブで、ワーカーが拾える状態のものです。
  • solid_queue_scheduled_executions: スケジュールされたジョブを含みます。まだ実行タイミングには達していません。
  • solid_queue_claimed_executions: ワーカーが実行のために確保)したジョブ情報を保存し、複数のワーカーが同じジョブを実行しないためです。
  • solid_queue_blocked_executions: ブロックされており、すぐに実行できないジョブを含みます。
  • solid_queue_failed_executions: 実行後にエラーになったジョブを保存し、監視やデバッグに役立ちます。

このように明確に複数のテーブルに設計されているため、Solid Queueは役割を明確に分離でき、ロジックがクリアになり、管理しやすくなります。

Solid Queueにおけるジョブのライフサイクル

Solid Queueの仕組みと、なぜ複数の異なるテーブルが必要なのかを理解するために、ジョブがエンキューされ、ワーカーに拾われ、実行され、削除されるまでの完全なライフサイクルを説明します。

1. ジョブが呼び出される時(エンキュー)

MyJob.perform_later(args) を呼び出すと、Solid Queueはデータベースに対して2つの書き込み操作を行います:

  • solid_queue_jobs テーブルへの書き込み:ジョブのメタデータ("queue_name", "class_name", "arguments", "priority", "active_job_id", "scheduled_at", "finished_at", "concurrency_key" など)を保存します。
  • すぐに実行するジョブの場合:solid_queue_ready_executions にデータを追加します。このテーブルには、ワーカーが処理可能な準備完了ジョブが含まれます。

2. ワーカーが実行するジョブを探す(ポーリング)

ワーカーは solid_queue_ready_executions テーブルを継続的に「ポーリング」して、新しいジョブを取得します。ワーカーは以下の2つの作業を行います:

  1. 確保: ワーカーが solid_queue_ready_executions からジョブを選択すると、solid_queue_claimed_executions テーブルにレコードを書き込みます。このレコードにより、2つのワーカーが同じジョブを実行することができません。
  2. 実行: クレームした後、ワーカーはジョブクラスの perform メソッドを呼び出して実行します。

3. ジョブ完了時、レコードの削除

ジョブが正常に実行されると、ワーカーは関連するすべてのテーブル(solid_queue_jobs, solid_queue_ready_executions, solid_queue_claimed_executions)からジョブを削除します。

ジョブのライフサイクルの簡単なまとめ

段階 関連テーブル 目的
ジョブのエンキュー solid_queue_jobs ジョブのメタデータを保存
ジョブ準備完了 solid_queue_ready_executions ワーカーが拾える状態
ワーカーによる確保 solid_queue_claimed_executions 1つのジョブを1つのワーカーが実行することを保証
実行 なし ワーカーが perform関数を呼び出す
完了 複数のテーブルから削除 レコードのクリーンアップ

安全性、ジョブの「失う」を防ぐ仕組み

重要な要件の一つは、エンキューされたジョブが少なくとも1回は実行され、失われないことです。Solid Queueは、ワーカーのクラッシュ、強制終了、プロセスの不具合などのケースを以下の形式で処理します:

  • 各ワーカーは起動時に solid_queue_processes にレコードを作成し、定期的に last_heartbeat_at を更新します。
  • ワーカーがジョブをクレームする際、solid_queue_claimed_executions にプロセスIDと共にレコードを書き込みます。
  • デフォルトはスーパーバイザープロセスがバックグラウンドで実行され、processes テーブルをチェックします。許容時間を超えて heartbeatがないプロセス(例:5分以上)が見つかった場合、それを「失敗したワーカー」と見なします。
  • スーパーバイザーはそのプロセスを削除し、そのワーカーが保持していたジョブを ready キューに再エンキューして、他のワーカーが拾えるようにします。

これにより、ワーカーがクラッシュしてもジョブは失われず、データの整合性が保証されます。

Solid Queue と Sidekiq の比較

Solid QueueとSidekiqはどちらもRailsで人気のある非同期処理のシステムですが、以下の表で違いを明確にします。

基準 Solid Queue (Rails 8) Sidekiq
ストレージバックエンド データベース (PostgreSQL / MySQL / SQLite) Redis (インメモリ、非常に高速)
Railsとの統合 ネイティブ、Rails 8からの公式組み込み コアじゃない、gem経由で使用
パフォーマンス 小〜中規模のワークロードに良好 非常に高い、大規模ワークロードに最適
遅延 DB使用のため比較的高い 低い (Redis インメモリ)
インストール 簡単、補助サービス不要 複雑、RedisとSidekiqの設定が必要
運用コスト ほぼゼロ (既存DBを使用) Redisのコストがかかる (特に本番環境)
信頼性 高い (SQLトランザクション + ジョブクレーム) 非常に高いがRedisに依存
リトライのロジック あり、DBに保存 あり、強力かつ柔軟
ダッシュボード 強力なUIはまだない Web UIが充実、リアルタイム監視が可能

いつ Solid Queue を選ぶべきか?

シンプル、軽量、ネイティブ、コスト節約を望むならSolid Queueを選びましょう。

いつ Sidekiq を選ぶべきか?

高速、強力、大規模システムに適したものを望むならSidekiqを選びましょう。

結論

Solid Queueは、インフラを簡素化し、Rails 8の大きな進歩を示しています。バックグラウンドジョブをコアフレームワークに直接統合することで、中小規模のプロジェクトはRedisやSidekiqに依存する必要がなくなり、安定性、信頼性の高いジョブ処理能力を確保しながら、運用コストを大幅に削減できます。

明日の記事の担当はエンジニアの宮川さんです。お楽しみに。

アジャイルは会社ごとに別物。でも、あるあるは共通だった

こんにちは、BUYMA TRAVEL Webエンジニア の赤間です。 

この記事はEnigmo Advent Calendar 202520日目の記事です。

この記事では、転職をきっかけに感じたことを基に、アジャイルスクラムの基本と、現場で起きがちな"あるある"とその対策について紹介します。

軽く自己紹介になりますが、私は2025年8月に転職してきたエンジニアです。前職でもエンジニアとして開発を行なっており、時期によってはスクラムマスターの役割も担当していました。

その経験から、転職後に「これってアジャイルか?」と戸惑ったことがあり、同じように悩む人のヒントになればと思いこの記事を書いています。

1. はじめに: 同じ「アジャイル」なのに、転職したら別物だった

前職では「スクラム」を実践していました。1週間という短いスプリントで開発・スプリントレビュー・ふりかえりを繰り返し、要件定義も(プロジェクト毎に)持ち回りで実施していました。

ところが転職後、同じく「アジャイル」を実践する現場に入ったものの、運用はスプリントよりも「この機能をいつ出すか」というリリース単位が中心です。参加した当初、実装やリリーススピードは前より速いはずなのに、私自身はどこか噛み合わず、「これってアジャイルなのかな?」と戸惑いました。いま考えると、アジャイルの形が違うというより、最適化している対象が違ったのだと思います。

この記事では、まずアジャイル/スクラムの基本をできるだけわかりやすく整理します。そのうえで、転職前後の現場を例に「同じアジャイルでも会社 (チーム) でこう違う」を簡単に比較し、それぞれの特徴や転職を通して見つけたよくある課題(あるある)と解決策を、アジャイルのことを知らない人にも伝わる形でまとめていきます。

2. そもそもアジャイルとは

アジャイルは一言でいうと、変化を前提に「小さく作って試し、フィードバックを受け軌道修正する」開発の考え方です。

最初に要件を固めて、計画通りに作り切る (いわゆるウォーターフォール) と対比するとイメージしやすいと思います。

 

ここで大事なのは、朝会・夕会・カンバンのような手段そのものではなく、フィードバックを得て、次に反映するサイクルが回っているかです。

たとえば「作ったものを早めに見せる → 反応をもらう → 次の方針を変える」というループが速ければ速いほど、価値や計画のズレが小さいまま進行できます。

3. スクラムとは

スクラムは、アジャイルの考え方を現場でうまく回すための代表的なフレームワークです。

アジャイル=考え方」だとすると、スクラムはそれを実践するために、役割・イベント (会議) ・成果物をセットで定義し、チームが迷いにくい形にしたもの、と考えるとわかりやすいです。

 

スクラムの用語

  • スプリント: 固定期間 (一般に1〜4週間) で区切られた開発サイクル

  • スプリントゴール: そのスプリントで達成したい目的 (「何のためにやるか」の軸)

  • プランニング: 次のスプリントで「何をどれだけやるか」をチームで決める

  • デイリースクラム: スプリントゴールに向けて、進捗確認と調整を行う毎日の短い打ち合わせ

  • スプリントレビュー: 出来上がった成果物を共有し、フィードバックをもらう場

  • レトロスペクティブ (ふりかえり) : やり方のカイゼンを話し合う場

 

重要: スクラム儀式ではなく、検証とカイゼンを回す仕組み

 

スクラムは「イベントをこなすこと」が目的ではありません。

短い周期で 検査 (Inspect) = いま正しい方向に進んでいるかを確かめ、適応 (Adapt) = 必要ならやり方・優先順位・計画を変える、という検証と改善を回すための仕組みです。

 

つまり、スクラムの各イベントは全て「Inspect / Adapt」のためにあります。

スプリントで区切ることも、スプリントレビューで見せることも、ふりかえりをすることも、全て価値や計画のズレを小さくするためです。

4. 前職スクラムの特徴 (メリット・デメリット)

  • 6人チーム/分業なし

    • + 依存が減って、詰まっても助け合いやすい (柔軟に回る)

    • - 何でも屋になりやすく、社内での育成コストと属人化リスクが上がる

  • 要件定義・見積もりが持ち回り

    • プロダクト理解が深まり、当事者意識が育つ

    • 得意不得意の差が出やすく、ブレや認識差が起きることも

  • スプリント1週間/ふりかえり重視

    • 追われやすく、レビュー品質が落ちると「忙しいだけ」になりがち

  • 朝会夕会で進捗確認

    • 見える化が効き、抱え込みや遅延を早く発見できる

    • 運用次第で報告会・監視っぽくなり、心理的安全性を下げる可能性あり

5. 現職アジャイルの特徴 (メリット・デメリット)

  • エンジニア4人+周辺職種は別チームで参加

    • 必要な専門性が適切なタイミングで入り、品質が上がりやすい

    • 意思決定や仕様の往復が増えると、スピードが落ちることがある

  • 半分業 (フロント/サーバ/インフラ) 

    • 専門性が積み上がり、品質とスピードを出しやすい

    • ボトルネックが固定化すると、待ちが増えてリードタイムが伸びやすい

  • 要件定義はCSが主導、デザイナーやエンジニアがブラッシュアップ

    • 顧客の声が仕様の入口にあり、「作ったけど使われない」を減らしやすい

    • 技術制約・実現方法の検討が遅れると、手戻りが増えることがある

  • スプリントが長期間

    • 機能にフォーカスしやすく、リリース目的がブレにくい

    • フィードバックが遅れると、気づいた時にはズレが大きくなっている

  • 毎日夕会/週1でふりかえり (乖離確認) 

    • 見える化が効き、抱え込みや遅延を早く発見できる

    • 運用次第で報告会・監視っぽくなり、心理的安全性を下げる可能性あり

6. スクラムの「ズレやすいポイント」あるある

ここまでアジャイル/スクラムの概要と、前職・現職それぞれの特徴を書きました。

面白いのは、運用の形は違っていても、実際に現場でつまずきやすい「ズレやすいポイント」には共通点があったことです。

ここからは、スクラムを回すときに起きがちな"あるある"を整理していきます。

1)  スプリントが長くなる/ズレ続ける

本来スプリントは固定期間で区切りますが、実際には意外とズレがちです。

祝日やメンバーの休み、突発対応、大きなリリースが重なると、「期間は決めているのに、結局終わらない」という問題が発生します。

この状態が続くと、スプリントレビューやふりかえりのタイミングが曖昧になってしまいます。

2) どうしても納期が先に確定してしまう

色々な要因で、納期が先に決まること自体は珍しくないと思います。問題は、納期が固定なのにスコープも固定になっていることです。この場合、現場がデスマーチになりやすいです。

3) 会議 (夕会) はあるが、スプリントレビューで顧客フィードバックが薄い

夕会などで進捗は把握できていても、スプリントレビューで"価値"を確かめられないと、やっていることはただの「進捗管理」になってしまいます。

その結果、予定通り作ったのに、想定していた価値が出ないというズレが発生しやすくなります。

4) ふりかえりはするが、カイゼンが実験になっていない

ふりかえり自体はやっていても、内容が「反省会」になってしまう。

ふりかえりの狙いは、誰かを責めることではなく、仕組みを少しずつカイゼンすることです。

5) 分業で詰まりが固定化する

分業は専門性を伸ばしやすい一方で、特定領域にタスクが集中すると、そこがボトルネックになってしまいます。

スクラムでは、個人の稼働率よりも、チームとしてのリードタイムが重要になるため、ここがズレの原因になりがちです。

7. 「じゃあどうすれば?」具体的なカイゼン

では、こうした"あるある"はどう解消すればいいでしょうか。

ここからは、私なりに考えたカイゼン案を紹介します。前職、現職で実際にチームで議論して試した工夫も、一部取り入れています。

スクラムの型に無理やり戻すことが目的ではありません。Inspect / Adapt (検査と適応) がきちんと回る状態に近づけることが目的です。

1)  スプリントが長くなる/ズレ続ける

スプリントの価値は"期限内に全部終える"ではなく、"短い周期での検証とカイゼン"にある

  • 祝日が多い週は最初からタスク数を減らす (期間は固定) 
  • どうしても溢れるなら、「終わらせる」ではなくスプリントを中止する

2) どうしても納期が先に確定してしまう

納期固定を成立させる条件は「変更できる何か (スコープ/品質/順序) 」があること

  • Must/Should/Could で削れる部分を先に決めておく
  • 「この日までにここまで」ではなく「この日までに価値が出る最小形」を合意する

3) 会議 (夕会) はあるが、スプリントレビューで顧客フィードバックが薄い

フィードバックが薄いと、"作ったものが刺さらない"がよく起きる

  • スプリントレビューは「説明」より「動くもの」を中心にする
  • 参加者が広げづらいなら、CSや営業から顧客の反応を持ち込むだけでもOK

4) ふりかえりはするが、カイゼンが実験になっていない

ふりかえりは反省会ではなく、カイゼンのA/Bテストに近い

  • 毎回、カイゼンアクションは1つ程度に絞る
  • 次回のふりかえりで「やったかどうか、効いたかどうか」を確認し、効かなければそのカイゼンはやめる
  • 見積もりが外れた原因は「前提が変わった」「分割が大きい」など仕組み側として考える

5) 分業で詰まりが固定化する

個人最適より、チームのリードタイム最適を狙う

  • 着手しすぎをやめる
  • 詰まりやすい領域は、スプリントレビュー待ちを減らすためにペア/モブを試す

8. 今後の展望

転職直後に「同じアジャイルなのに、なんだか噛み合わない」と感じたのは、今思えば当然でした。

前職で体験していたのは、短いスプリントでレビューとふりかえりを回し続ける"スクラム寄りのアジャイル"。一方で現職は、機能リリースを軸に、CSや周辺職種の知見も取り込みながら進める"プロダクト寄りのアジャイル"です。言葉は同じでも、狙っている最適解が少し違っていたんだと思います。

 

アジャイルスクラムに"唯一の正解"はありません。会社のフェーズ、プロダクトの性質、チームの人数やスキルで、うまく回る形は変わります。大事なのは「どの型が正しいか」を決めることではなく、いまの自分たちにとって必要なカイゼンを見つけて、小さく試して、調整し続けること。そのプロセス自体が、アジャイルの面白さだと感じています。

 

今後も現職の強み (専門性・顧客起点・リリース志向) を活かしながら、ズレが大きくなるポイントを修正していきます。

 

明日の記事担当はBUYMAのWebエンジニアレミーさんです。お楽しみに。

機械学習実験を加速させる dbt による特徴量管理の実践

こんにちは、AI テクノロジーグループ データサイエンティストの髙橋です。業務では企画/分析/機械学習モデル作成/プロダクション向けの実装/効果検証を一貫して行っています。この記事は Enigmo Advent Calendar 2025 の 19 日目の記事です。

本記事では、 dbt を利用した機械学習モデルの特徴量管理について紹介します。この特徴量管理を活用することで、機械学習を利用したプロジェクトで多くの実験を効率的に実施でき、利益増加というビジネス成果に繋げることができました。

www.getdbt.com

※文中に記載するディレクトリやファイル名、 SQL コード、コマンドなどは全てイメージです。

特徴量管理の目的・成果

dbt を利用した特徴量管理を導入した目的は、ある機械学習プロジェクトにて効率的に数多くの特徴量を試す必要があったためです。まず、そのプロジェクトと得られた成果について説明します。
弊社が運営している CtoC ECサービス BUYMA では、MA (Marketing Automation) ツールを通じてクーポンなどのインセンティブをメールで配布しています。これまで、様々な会員セグメントを定義し、各セグメントに対してのインセンティブ配布ルールの運用を行っており、そのルールのチューニングで改善を図っていました。このアプローチではルールをもとに一律配布しているため、機械学習による最適化余地があると考えました。具体的には、インセンティブがなくても購入する会員にもコストをかけてしまったり、インセンティブがあれば購入する会員に配布できていなかったりなどの最適化の余地があるのではないかと考えました。そこで、機械学習を活用してデータに基づくより効果的なインセンティブ配布を実現することを目指しました。

BUYMA はリリースから 20年を超えるサービスであり、様々な MA シナリオ(会員セグメントと配布ルールの組み合わせを以降 MA シナリオと呼びます)が運用されています。出来るだけ多くの MA シナリオに対して機械学習による最適化を適用したく、そのためには様々な特徴量を効率良く試せるようにすべきと考えました。

そこで、今回紹介する dbt を利用した特徴量管理を導入しました。その結果、約1年間でおよそ8個の MA シナリオに機械学習による配布最適化を試すことができ、シナリオによってばらつきはあるものの約20%の利益増加が実現できました。

数多くの特徴量を試す上での課題

まず前提として、特徴量は BigQuery で作成する方針としました。理由は、既に BUYMA のデータは BigQuery に保存されていたことと、Python 実行環境(ノートブックなど)への特徴量作成のもとになる行動データのダウンロードに非常に時間がかかったためです。時間がかかる理由は、BUYMA は会員数・商品数が非常に多く、それに伴いユーザーの行動ログのデータ量も非常に多いためです。具体的には、会員数1185万人、商品数590万品であり*1、利用する行動ログのレコード数は数千万件になることもあります。

この前提のもと、BigQuery で数多くの特徴量を効率的に試すには以下の課題がありました。

  • 特徴量の数が多くなると SQL が肥大化して可読性が低下する。
    • 例えば、弊社で過去別の機能で作成した特徴量 SQL ファイルは2000行を超えており読むのが大変でした。
  • 特徴量作成のロジックが複雑になると可読性が低下する
    • 例えば、過去 n 日間の閲覧数という特徴量を複数 n に対して記述すると、SQL が長くなり可読性が低下します。
  • 別の実験で特徴量を再利用しづらい
    • 再利用する場合は、その特徴量部分を毎回コピペする必要があるためです。

dbt による課題解決

そこで、 dbt を利用して特徴量管理を行うことで、これら課題の解決を図りました

まず、 dbt について簡単に説明します。ただし、dbt は多くの機能があるため今回の課題解決に関連する機能に絞って説明します。全体を詳しく知りたい方は公式ドキュメントを参照ください。 今回役立ったのは以下の機能です。

  • SQL ファイルの分割
  • for などのロジックの記述

SQL ファイルの分割について、dbt を利用することで CTE (Common Table Expression) を別のファイルに分けることができます。例として、以下のような CTE を使った SQL を考えます。

-- main.sql
WITH users AS (
    SELECT
        id AS user_id,
        first_name,
        last_name
    FROM `project.dataset.users`
)

SELECT
    *
FROM
    users

dbt を利用することでこの SQL を2つのファイルに分けることができます。

-- user.sql
SELECT
    id AS user_id,
    first_name,
    last_name
FROM `project.dataset.users`

-- main.sql
SELECT
    *
FROM
    {{ ref("user") }}

ここで、 {{ (ref("user")) }} は user.sql を参照することを意味します。 dbt run コマンドを実行すると、 user.sqlmain.sql のテーブルビューが作成されます。この機能を利用することで、特徴量作成などの SQL を分割して可読性を向上させることが出来ます。具体的には、以下のように SQL ファイルを分割しました。

models
├── datasets
│   └── dataset.sql
└── features
│   ├── features_user_attributes.sql
│   └── features_user_action_log.sql
└── labels
    ├── label_type_1.sql
    └── label_type_2.sql

ここで、 models ディレクトリは dbt でデータ取得のための SQL を配置するディレクトリです。*2

各 datasets ファイルの中身は以下のようにしました。

SELECT
    *
FROM
    {{ var("user_ids_table_name") }}
LEFT JOIN
    {{ ref("features_user_attributes") }}
USING(user_id)
LEFT JOIN
    {{ ref("features_user_action_log") }}
USING(user_id)
LEFT JOIN
    {{ ref("label_type_1") }}
USING(user_id)

ここで、 var は dbt で利用できる変数です。*3 様々な会員セグメントについて実験するために、セグメントごとの会員 ID を別テーブルにあらかじめ保存しておき、変数として切り替えられるようにしました。 features ディレクトリ配下のファイルは意味がある粒度で特徴量を分けて再利用しやすくしました。また、機械学習の目的変数である label も複数パターン試せるようにしました。 こうしたことで、実験が進むごとに特徴量が増加しても、SQL が肥大化して読みにくくなることを防げました。具体的には、1つの SQL ファイルあたり長くとも約100行におさまるようになりました。また、 dataset.sql を見ればどのような特徴量が利用されているかが一目で分かるようになりました。

for などのロジックの記述について、dbt では Jinjaというテンプレートエンジン の記法で for などのロジックを記述することができます。これを利用して、例えば過去 1、3、7日間の閲覧、お気に入り回数の集計は以下のように記述できます。

{%- set agg_actions = ["view", "like"] -%}
{%- set last_n_days = [1, 3, 7] -%}

SELECT
    user_id,
    {% for agg_action in agg_actions %}
        {% for last_n_day in last_n_days %}
            COUNTIF(
                day_from_base_date <= {{ last_n_day }}
                AND action = "{{ agg_action }}"
            ) AS cnt_action_{{ agg_action }}_last_{{ last_n_day }}_days,
        {% endfor %}
    {% endfor %}
FROM
    `project.dataset.user_action_log`
GROUP BY
    user_id

ここで、簡単のために user_action_log テーブルに特徴量作成の基準日から何日前のログかを示す day_from_base_date カラムが存在すると仮定しています。これを通常の SQL で記述すると以下のようになります。

SELECT
    user_id,
    COUNTIF(
        day_from_base_date <= 1
        AND action = "view"
    ) AS cnt_action_view_last_1_days,
    COUNTIF(
        day_from_base_date <= 3
        AND action = "view"
    ) AS cnt_action_view_last_3_days,
    COUNTIF(
        day_from_base_date <= 7
        AND action = "view"
    ) AS cnt_action_view_last_7_days,
    COUNTIF(
        day_from_base_date <= 1
        AND action = "like"
    ) AS cnt_action_like_last_1_days,
    COUNTIF(
        day_from_base_date <= 3
        AND action = "like"
    ) AS cnt_action_like_last_3_days,
    COUNTIF(
        day_from_base_date <= 7
        AND action = "like"
    ) AS cnt_action_like_last_7_days
FROM
    `project.dataset.user_action_log`
GROUP BY
    user_id

比較してみると、 dbt を利用することで SQL が短くなり、かつ変数を定義できるためどの行動を過去何日分集計するかが一目で分かるようになりました。これにより特徴量が複雑になっても可読性が低下することを防げました。また、集計する日数や行動が増えたとしても、変数のリストに要素を追加するだけで対応できるようになりました。

dbt による特徴量管理時の工夫

より多くの特徴量を素早く試せるように行った工夫があるため、それらも紹介します。ここでは2つ紹介します。

1つ目はデータセット管理表を用意し、実験で利用するデータセットごとに ID を採番し、 dataset_001dataset_002 のようにファイルを作成していく方針としたことです。

データセット管理表のイメージ:

データセットID データセット説明
1 セグメント1に対して特徴量 A を利用したデータセット
2 セグメント2に対して特徴量 A, B を利用したデータセット

作成したファイルのイメージ:

models
└── datasets
    ├── dataset_001.sql
    └── dataset_002.sql

こうすることで、新しいデータセットを簡単に追加できるようにし、かつ過去のデータセットも参照しやすくしました。実際に2025年12月時点ではデータセット ID は 100 を超えていますが、問題なく運用出来ています。

2つ目は dbt で作成したテーブルのビューから Python 実行環境でデータを取得する際は、以下のような SQL で一度 GCS にエクスポートしてダウンロードするようにしたことです。これは、Python で BigQuery SDK を利用してデータ取得するとレコード数が多い場合非常に時間がかかるためです。

-- analyses/export_dataset.sql
{%-
    set bucket_folder = "datasets/" + var("dataset_id")
-%}
{%-
    set table_name = 
        target.database
        + "."
        + target.schema
        + "."
        + "dataset_"
        + var("dataset_id")
-%}

-- BigQuery の export data 構文において _table_suffix を含んでいるとエラーが発生するため
-- CREATE TEMP TABLE 構文を利用。
-- https://stackoverflow.com/a/70033601
CREATE TEMP TABLE temp_dataset AS (
    SELECT
        *
    FROM
        {{ table_name }}
);

EXPORT DATA
OPTIONS (
    uri = "gs://{{ var('bucket_name') }}/{{ bucket_folder }}/*.gz",
    format = "Parquet",
    overwrite = true,
    compression = "GZIP"
)
AS (
    SELECT
        *
    FROM
        temp_dataset
);

ここで、GCS バケット名やフォルダ、エクスポートするデータセット ID を dbt 変数としており、これによりデータセットによってエクスポート先を変更できるようにしました。また、この SQL はテーブルビューを作成する必要がないため、 analyses ディレクトリに配置して、以下のコマンドでコンパイルして実行するようにしました。

dbt compile \
    --select analyses/export_dataset.sql \
    --vars '{bucket_name: "your_bucket_name", bucket_folder: "your_bucket_folder", dataset_id: "001"}' && \
bq query < target/compiled/your_dbt_project_name/analyses/export_dataset.sql

ここで、 dbt compile コマンドは Jinja 記法などを解決して実行可能な SQLコンパイルするコマンドであり、コンパイルされたファイルは target/compiled 配下に保存されます。また、 dbt の analyses ディレクトリとは models ディレクトリとは異なり一時的な分析用 SQL などを配置するのに適したものになります。*4

まとめ

本記事では、dbt を利用した特徴量管理について紹介しました。SQL の肥大化や特徴量の再利用しづらさという課題を、 SQL ファイルの分割や for などのロジック記述により解決しました。また、データセット管理の方法や Python 実行環境でのデータ取得の高速化というより効率的に多くの特徴量を試す方法も紹介しました。これにより、複数の MA シナリオに対して機械学習を利用したインセンティブ配布最適化を試すことができ、利益増加という成果に繋げることが出来ました。

本記事が特徴量管理の参考になれば幸いです。

明日の記事は BUYMA TRAVEL のエンジニアの赤間さんです。お楽しみに!


株式会社エニグモ すべての求人一覧 hrmos.co

*1:2025年10月末時点の数値です。https://enigmo.co.jp/ir/

*2:dbt models について詳細は dbt 公式ドキュメントを参照ください。

*3:dbt の変数について詳細は dbt 公式ドキュメントを参照ください。

*4:dbt analyses について詳細は dbt 公式ドキュメントを参照ください。

AWS Savings Plans検討をGemini Gemsで自動化する~~プロンプト作成のコツと組織管理の課題~~

こんにちは、インフラエンジニアの森田です。

この記事はEnigmo Advent Calendar 2025の 14日目の記事です。
今回は、業務効率化のためにGoogle Geminiのカスタム指示(Gems)を作成し、
実際の業務で使ってみた使用感や気づきについて紹介します。

どのような業務に活用したか

私は直近でAWSのコスト削減に取り組んでいます。
特にSavings Plansなどを購入する際、複数アカウントのオンデマンドコストと推奨コミット額を見比べ、
その購入計画が適切かを整理する必要がありました。
これを人力で行うのは工数もかかり、ミスのリスクもあるため「辛い」作業でした。

そこで、Savings Plans推奨事項のCSVファイルと、
Cost ExplorerからCLIで取得したJSONファイルを読み込ませることで、
コミット額の適切性検証とコストメリットの整理を行ってくれるGemsを作成しました。

結果として、非常に良好な感触を得られました。
やはり、複雑な数値の突き合わせや計算は計算機(AI)に任せるのが一番です。
本記事では、実際にツールを作ってみて「気をつけると良い点」と、
組織で運用する上で「課題だと感じた点」を共有します。

カスタム指示(Gems)作成のポイント

コスト削減アシスタントGemsを作成する過程で、特に重要だと感じたポイントは以下の3点です。

1. 具体的な使い方の説明(ガイド)を含める

スクリプトと異なり、対話形式で進むため、
初見のユーザーでも迷わないよう「利用手順」を指示に含めておくと親切です。
今回はデータを読み込ませて分析するツールなので、
以下のようにデータの取得手順を案内させるようにしました。

## 0. ユーザーサポート / 使い方ガイド

ユーザーから「使い方を教えて」「何が必要?」と問われた場合、または挨拶のみでデータが未提供の場合は、以下の3ステップのデータ取得手順を案内してください。

### 手順1: 推奨事項CSVのダウンロード (AWS Console)
<取得手順を記載>

### 手順2: 直近のオンデマンド料金取得 (AWS CLI)
<取得手順を記載>

### 手順3: データの取得
<取得手順を記載>

2. 入出力のイメージを厳密に定義する

曖昧な指示だと、実行のたびにAIの解釈が変わり、
出力フォーマットがブレて使いづらくなります。
AIに勝手な解釈をさせないよう、入力データの処理ルールと出力形式を
以下のように固定することをお勧めします。

入力ルールの例:

## 2. 入力データの処理ルール

以下のデータがテキストまたはファイルとして与えられます。

1. **推奨事項 (CSV):** `savings-plans-recommendations.csv`
    * ここから「アカウントID」「推奨コミットメント額($/h)」「推定削減率」を抽出します。

2. **実績コスト (JSON):** `ec2_ondemand_daily_filtered.json` (Cost Explorer出力)
    * **安全性判定:** `推奨コミットメント額($/h) × 24h``日次実績コスト(過去30日間の最小値)` を下回っているか確認してください。実績を下回っていれば「安全(使い切れる)」、上回っていれば「注意(使い切れないリスクあり)」と判定します。
(略)

出力ルールの例:

必ず以下の **【出力1】** 〜 **【出力3】** の形式で出力してください。

-----

### 【出力1】
<出力1の構造を指示する>
(略)

3. 複雑なファイルは「キャプチャ画像」で読ませる

複雑なレイアウトのExcelやPDFファイルは、
テキスト抽出時に構造が崩れ、正しく解析できない場合があります。
そのような場合、対象箇所のキャプチャ画像を撮って画像を読ませる方が、
精度が高くなるケースがありました。
テキストでの読み込みで精度が出ない場合は、「画像を読ませる」という選択肢を
頭の片隅に置いておくと良いでしょう。

管理・運用上の懸念点

個人のツールとしては優秀なGemsですが、これを「会社の資産」として管理しようとした際、
いくつか課題も見えてきました。

変更履歴が見えない

作り込んだカスタム指示は長文になりがちですが、現状のGemsには変更履歴(Diff)を見る機能がありません。
「誰が・いつ・なぜ変更したか」が追えないため、チーム開発には不向きです。
GCPのVertex AI AgentsであればTerraform等で管理可能ですが、
Gemini(Gems)単体では難しいため、現状は「プロンプトの内容をテキストファイルとしてGitで管理し、変更時はGitを通してから手動でGemsを更新する」という運用が現実解になりそうです。
スマートではありませんが、資産管理としては必要です。

作成者のアカウント削除でGemsも消える

Gemsの実体は、作成者の「マイドライブ/Gemini Gems/」配下に保存されるファイルとして扱われるようです。
そのため、作成者が退職等でアカウント削除されると、
マイドライブ内のデータと共にGemsも消失してしまいます。
これを回避するために共有ドライブへの集約を試みましたが、
共有ドライブ上のGemsファイルを開こうとするとエラーが発生しました(下図参照)。
現状では、誰か個人のマイドライブに配置されている必要がありそうです。

共有ドライブ上のGemsを開いた際のエラー

モデル更新による挙動の変化(AIドリフト)

これはLLMを利用する全般的なリスクですが、バックエンドのモデルが更新された際、
以前と同じプロンプトでも挙動が変わる可能性があります。
ChatGPTのCustom GPTsのようにモデルバージョンを固定する機能は、現状のGemsには見当たりません。
影響を最小限にするためには、前述の通り「入出力を厳密に定義」してAIの解釈の幅を狭めておくことが重要です。
また、モデル更新のアナウンスがあった際は、簡単な動作確認フローを設けるのが良いでしょう。

まとめ

スプレッドシートでオンデマンドコストとコミットコストを整理して購入計画を立てていたときは2,3日かかっていたところ、
業務特化型のGemsを作成することで、正味1時間あれば整理が完了するようになり大幅に効率化することができました。

一方で、チームや組織で永続的に管理・運用していくには、
バージョン管理やオーナー権限の面でまだ工夫が必要だと感じています。

今後、Google Workspaceの機能アップデートにより、
これらの管理機能が強化されることを期待しつつ、
まずはGit管理などの運用ルールでカバーしながら活用していきたいと思います。

「AIでさがす」サービスのリニューアル - BUYMA内記事コンテンツをベースにした商品提案エージェントの実現

こんにちは、AIテクノロジーグループのエンジニアの吉田です。

本記事はEnigmo Advent Calendar 2025の 18日目の記事です。

普段は検索システム全般、機械学習システムのMLOps、AI関連の機能開発を担当しております。

この記事では「AIでさがす」サービスのリニューアルについて紹介します。

「AIでさがす」サービスとは

「AIでさがす」サービスは、BUYMAのWebサイトおよびアプリで提供している、AIを活用した商品提案サービスです。

実際の機能は以下からご利用頂けます。(BUYMAアカウントでのログインが必要となります。)

「AIでさがす」サービス

ユーザーが文章で質問すると、AIが質問内容を理解し、おすすめの商品を提案します。例えば「春のデートにぴったりなワンピースを教えて」といった質問に対して、AIが回答文とともに具体的な商品を紹介します。

従来のキーワード検索では見つけにくかった商品や、ユーザー自身が気づいていなかった新しい商品との出会いを提供することで、BUYMAでのショッピング体験をより豊かにすることを目指しています。

※商品画像はモザイク加工しております。

リニューアルの背景

旧システムは、ChatGPT APIを活用した商品提案サービスでしたが、主な課題が3点ありました。

  • BUYMAの知識不足
    • ChatGPT が一般的な知識で回答を生成するため、BUYMAならではのトレンドや商品特性を反映できない。
  • 根拠の不明確さ
  • 検索キーワード生成の精度
    • 形態素解析ツールの MeCab を併用していましたが、文脈や意味を理解した検索キーワード生成ができない。

また、リリースから2年が経過し、本格的にバージョンアップが必要なタイミングでもありました。

※旧システムの詳細はこちらの記事で紹介しております。

ちょうどチームメンバーが社内ドキュメントのAI検索システムを開発しており、この仕組みをBUYMAの多数の記事コンテンツに適用すれば、よりBUYMAらしい商品提案が可能になると考えました。

そこで、今回のリニューアルでは、BUYMA内の記事コンテンツ群をベースに会話するエージェントを作成しました。これにより、BUYMAならではの知識を持ったAIが、よりBUYMAでおすすめしたい商品を提案できるようになりました。

システム変更前後の比較

旧システムと新システムの違いは以下の通りです。

旧システムでは、ChatGPT が一般的な知識で回答を生成し、MeCab による単純な形態素解析で検索キーワードを生成していました。そのため、BUYMAならではの文脈を理解した商品提案が難しい状況でした。

新システムでは、BUYMA内記事コンテンツを参照した Vertex AI Search が回答文を生成し、Gemini が文脈を理解した検索キーワードを生成します。その結果、よりBUYMAらしい商品提案が可能になりました。

それぞれの処理フローは以下の通りです。

旧システム処理フロー

  1. BUYMA基幹システムから「AIでさがす」APIにリクエスト
  2. 「AIでさがす」APIがユーザーの質問を ChatGPT APIに送信
  3. ChatGPT が回答文とおすすめアイテムリストを生成
  4. アイテム名を MeCab形態素解析)で解析し、検索キーワードを生成
  5. 検索APIで商品情報を取得し、ユーザーに表示

新システム処理フロー

  1. BUYMA基幹システムから「AIでさがす」APIにリクエスト
  2. 「AIでさがす」APIがユーザーの質問を Vertex AI Search に送信
  3. Vertex AI Search (事前にBUYMA内記事コンテンツをインポート済み)が回答文を生成
  4. 質問文と回答文を Gemini に送信し、検索キーワードを生成
  5. 検索APIで商品情報を取得し、ユーザーに表示

アーキテクチャー特徴

Vertex AI Search を利用して、インポートしたBUYMA内記事コンテンツをベースに会話を行うエージェントを構築しました。

  • BUYMA内記事コンテンツのインポート
    • 約4000件の記事をデータストアにインポート
  • プロンプト設計
    • 「ファッションECサイトBUYMAのショッピングアドバイザー」として定義し、ユーザーの質問に対して最適な商品を提案する形で回答を生成

2. Gemini

Gemini を活用する事により、会話内容から商品検索キーワードを生成する機能を作成しました。

  • プロンプト設計
    • ECサイトの検索キーワードを生成する専門家」として定義し、会話の文脈を理解して検索キーワードを生成
  • MeCab との違い
    • MeCab は単語の分解のみだが、Gemini は文脈を理解してブランド名・カテゴリ名・モデル名を組み合わせた検索キーワードを生成

実装時の課題・解決策・工夫した点

  • Vertex AI Search の幻覚への対応

    • 初回質問時に Vertex AI Search が過去から質問が続いているような幻覚を見る場合がありました。当初は初回と2回目以降の会話を共通のプロンプトで行っており、「ユーザーの過去の質問履歴」の項目に入っている文言の有無から初回なのか、2回目以降の会話なのかを判断する指示を出していました。ところが、「過去」という文言に引きずられてなのか、初回なのに過去の質問をAI側が捏造して、その続きとして回答する場合が稀にありました。
    • プロンプトテンプレートを初回用と2回目以降用の2種類に分け、初回用のプロンプトからは「ユーザーの過去の質問履歴」の文言自体を削除する事によって対応しました。
  • 敵対的クエリへの対応

    • 敵対的クエリ(不適切な質問)の場合、Vertex AI Search のAPIからのレスポンスフォーマットが通常とは異なるものになり、要約が生成できないにもかかわらず、無理やり商品紹介を行ってしまいました。
    • 敵対的クエリーのフォーマットを検知した場合は、要約失敗として扱い、商品紹介を行わないように修正しました。
    • この場合以外でも稀に異なるフォーマットのレスポンスになる場合があり、サービス継続に支障が出ないように都度改善を行いました。
  • Gemini のライブラリ移行

    • もともと使用していたライブラリがサポート終了を迎えるため、社内では実績がない新しいライブラリに移行する必要がありました。移行後、従来使用していた Gemini モデルが初期設定では使用できず、次世代のモデルを試したところレスポンスタイムが大幅に遅くなってしまいました。新しいライブラリという事もあり、AIツールではなかなか解決できず、最終的にはGoogleサポートに問い合わせして解決に至りました。

得られた学びとノウハウ

  • AIツールの活用と限界

    • 「AIでさがす」のバックエンドAPIリポジトリは、ほぼ全部作り直したのですが、AIツールを活用する事によって、工数を節約する事ができました。Terraform 関連のリソース修正、テストケース作成やMOCK用のフロントエンド実装等においてもAIツールにより大幅な工数削減ができました。
    • 一方で、Gemini のライブラリ移行など、ドキュメントの記載やインターネット上での知見が少ない領域ではAIツールでは解決できず、結果的に公式サポートへの問い合わせが必要でした。
  • AIの不確定な挙動への対応

    • Vertex AI Search の幻覚や部分的な失敗など、AIサービス特有の不確実性に対して、初回用と2回目以降用でテンプレートを分けるなど、細かな調整が重要でした。また、プロンプトだけではどうする事もできない場合があり、そのような場合は後処理でルールベースのロジックを追加する必要がありました。

効果測定

リニューアル後、以下のような指標が上昇しました。

  • 1スレッドあたりの質問数の平均
    • 会話の継続性が向上し、ユーザーが複数回質問を続けるようになりました。
  • 1ユーザー1日あたりの質問数
    • 利用頻度が向上し、ユーザーがより積極的に機能を活用するようになりました。
  • 検索URLに遷移された回数
    • 商品検索への誘導効果が向上し、実際の商品閲覧につながるケースが増加しました。

これらの結果から、BUYMA内記事コンテンツを根拠とした回答の提供と、文脈を理解した検索キーワード生成により、ユーザーの満足度と利用価値が向上したと考えられます。

直近の対応/今後の展開・課題

  • 金額絞り込み機能の追加(今月対応)

    • ユーザーからの要望が多い金額絞り込み機能の対応をしました。価格帯に関する質問に対して適切な商品提案ができていない課題があったため、Gemini で検索API用の金額フィルタークエリを生成することで対応しました。
  • コンテンツの拡充

    • 現在はBUYMA内記事コンテンツのみを Vertex AI Search にインポートしていますが、今後はYouTubeでの発信内容も追加する予定です。記事以外のコンテンツも活用することで、より幅広い情報をユーザーに提供できるようになります。
  • 継続的なメンテナンス

    • AIのライブラリやモデルは随時更新されていくため、継続的なメンテナンスが課題となります。特に Gemini や Vertex AI Search などのサービスは進化が早く、新しいモデルへの対応や非推奨ライブラリ/バージョンから移行など、定期的な見直しが必要です。

まとめ

本記事では、「AIでさがす」サービスのリニューアルについて紹介しました。

旧システムでは ChatGPT と MeCab を使用していましたが、BUYMA特有の知識不足や根拠の不明確さなどの課題がありました。リニューアルでは Vertex AI Search と Gemini を採用し、BUYMA内記事コンテンツを根拠とした回答生成と文脈を理解した検索キーワード生成を実現しました。

実装時には敵対的クエリへの対応やAIサービス特有の不確実性への対処など様々な課題に直面しましたが、ロジックでの細かい制御やAIツールの活用により解決できました。リニューアル後は会話継続性や利用頻度、商品検索への誘導効果が明らかに向上しています。

明日の記事は同じAIテクノロジーグループの髙橋さんです。お楽しみに。


株式会社エニグモ すべての求人一覧 hrmos.co

Workflows + Cloud Scheduler で定期処理をサーバーレス構築(Cloud Composer との比較もあります)

こんにちは、AIテクノロジーグループの太田です。
普段は商品のカタログデータ基盤を開発・運用するチームで業務に携わっております。エニグモではそういったデータやAI関連の技術基盤としてGCPを利用しており、そこで利用したWorkflowsについて紹介したいと思います。

この記事はEnigmo Advent Calendar 2025の17日目の記事です。

1. はじめに:なぜこの構成に至ったか

  • 背景
    毎日追加・更新される商品データを Vector Search のインデックスに反映させる必要があった。
    Cloud Composer (Airflow) の利用実績はあったものの、より安価な Workflows に興味があった。
  • 課題
    「差分抽出 → 画像処理(Embedding) → インデックス更新」という一連のフローを毎日決まった時間に実行したい。
    各処理ステップ(特に画像処理とインデックス更新)は時間がかかるため、タイムアウトやリトライ制御を考慮する必要がある。
    当然コストは抑えたい。
  • 結論
    Cloud Composer を使わず、Workflows + Cloud Scheduler を採用することで、管理コストと金銭的コストを最小限に抑えたアーキテクチャを構築した。

ポイントは、重たい処理(画像処理・インデックス更新)は Dataflow に任せ、Workflows はあくまで「順序制御」に徹する構成にしたことです。

2. Goolge Cloud 構成:全体アーキテクチャ概要

処理の流れは次のとおりです。

  • Cloud Scheduler 毎日定時に Workflows をトリガー。
  • Workflows: 全体の指揮者。以下のステップを順次実行。
    • BigQuery: 前日データとの差分をSQLで抽出。
    • Workflows: 画像の Embedding 計算とGCSへの保存を実行する Dataflow を実行する。
    • Workflows: Vector Search のインデックス更新を実行する Dataflow を実行する。

ポイントは、長時間実行かつ単発で実行する機会がある Dataflow の実行を別の Workflows に委ね、メインの Workflows から別の Workflows を呼び出すようにしたことです。

Dataflowの実装については、本記事の趣旨から外れるので省略いたします。

3. 技術選定:なぜ Cloud Composer ではなく Workflows なのか

このセクションで、他の選択肢と比較し、なぜ今回の構成に至ったかを解説します。

比較項目 Workflows Cloud Composer
特性 サーバーレスで軽量、直線的なフローに最適 複雑な依存関係に強いが、常時稼働が必要
コスト 安価(実行回数課金)
1000ステップ0.01ドル1
Google Cloud 外へのアクセスを要する場合は1000ステップ0.025ドル
高い(小規模でも月額数万円〜)2
実際に月額約8万円かかっています
採用/不採用の決め手 今回の処理が「直線的」であり、複雑なDAGが不要だったため 日次バッチ一つに対してはオーバースペック

運用実績があったからといって、「とりあえず Airflow」とせずに、ワークフローの複雑さに応じてツールを選定できた点が良かったです。

実際に使ってみて、単純な A -> B -> C というフローなら Workflows の方が圧倒的に運用・コストメリットが大きいことが実感できました。

4. 実装サンプル:Workflows から Dataflow を起動する

ここでは、実際に Workflows を使って Dataflow (Flex Template) を起動するための定義(YAML)を紹介します。

【コード解説のポイント】

Dataflow の Flex Template を利用することで、Docker イメージ化したジョブをパラメータ付きで呼び出せます。
Workflows 側でジョブの完了を待機する(ポーリングする)ようにしました。
googleapi 3 で Dataflow 以外の各種 Google Cloud のプロダクトへアクセスすることができるので、参照してみてください。

【サンプルコード(YAML)】

実際に作成した Dataflow を起動する Workflows の定義ファイル(main.yaml)の一部を掲載します。

  • main.yaml の抜粋イメージ
steps:
  - init:
      assign:
        - project_id: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
        - location: "asia-northeast1"
        - job_name: "dataflow-launcher"
        # デプロイするときに --set-env-vars で設定した環境変数をここで読み込む
        - sdk_container_image: ${sys.get_env("sdk_container_image")}
        - gcs_bucket: ${sys.get_env("gcs_bucket")}
        # YYYYMMDD 形式の日付
        - ymd: ${text.replace_all(text.split(time.format(sys.now()), "T")[0], "-", "")}
  
  # 画像処理をするので時間がかかる Dataflow を実行するステップ
  - dataflow_start_crop_embedding:
      call: googleapis.dataflow.v1b3.projects.locations.flexTemplates.launch
      args:
        projectId: ${project_id}
        location: ${location}
        body:
          launchParameter:
            jobName: ${job_name + ymd}
            containerSpecGcsPath: ${gcs_bucket + "/templates/your-template-spec.json"}
            parameters:
              sdk_container_image: ${sdk_container_image}
            environment:
              stagingLocation: ${gcs_bucket + "/templates/staging"}
              tempLocation: ${gcs_bucket + "/templates/temp"}
              serviceAccountEmail: ${Dataflow 実行権限を持つ Service Account}
      result: dataflow_result
      next: initialize_polling

  # 以下、Dataflow を完了まで監視するステップ
  # ループした回数だけステップ数が増えてコストも増えていくのでループ数には注意してください。

  - initialize_polling:
      assign:
        - counter: 0
        # "max_retries * 60秒 (wait_60_secondsで定義) = 1時間"なので、最大1時間ポーリングを行う。
        - max_retries: 60
      next: poll_job_status

  - poll_job_status:
      call: googleapis.dataflow.v1b3.projects.locations.jobs.get
      args:
        projectId: ${project_id}
        location: ${location}
        # run_dataflow_job ステップの return から参照できる。
        jobId: ${dataflow_result.job.id}
      result: job_status
      next: check_job_status

  - check_job_status:
      switch:
        - condition: ${job_status.currentState == "JOB_STATE_DONE"}
          next: job_succeeded
        - condition: ${job_status.currentState == "JOB_STATE_FAILED" or job_status.currentState == "JOB_STATE_CANCELLED"}
          raise: ${"Dataflowジョブ " + dataflow_result.job.id + " が失敗しました。ステータス " + job_status.currentState}
        - condition: ${counter >= max_retries}
          raise: ${"Dataflowジョブ " + dataflow_result.job.id + " が1時間以内に完了しませんでした。タイムアウト。"}
        - condition: ${true}
          next: increment_counter

  - increment_counter:
      assign:
        - counter: ${counter + 1}
      next: wait_60_seconds

  - wait_60_seconds:
      call: sys.sleep
      args:
        seconds: 60
      next: poll_job_status

  - job_succeeded:
      return: "SUCCESS"

【サンプルコードをデプロイ】

作成した Workflows の定義ファイルをデプロイ4します。

gcloud workflows deploy sample_workflow \
        --source=main.yaml \
        --location="asia-northeast1" \
        --project=${PROJECT_ID} \
        --service-account=${SERVICE_ACCOUNT_EMAIL} \
        --set-env-vars sdk_container_image=${Artifact Registry にpushしたdockerイメージ} \
        --set-env-vars gcs_bucket="gs://YOUR_BUCKET" \

【サンプルコードを実行した結果】

ループしているので実際に実行されたステップ数は137でした。

コストは 137 * 0.01 / 1000 = 0.00137 ドルになります。

5. Workflows を採用する上で許容した「不便な点」

コストと手軽さは魅力的ですが、導入に際しては以下のデメリットも考慮する必要がありました。

  1. 開発体験のクセ(YAML地獄)

    • 課題
      Python で記述できる Airflow と異なり、Workflows は YAML (または JSON) でロジックを記述する必要があります。条件分岐やループ処理、変数の扱いが直感的ではなく、構文エラーに悩まされることが多いです(一般的に "YAML engineering" と揶揄される部分)。
    • 対応
      今回は「直列的なフロー」に留めることで複雑な記述を回避しました。複雑なロジックが必要な場合は、無理に Workflows 内に書かず、Cloud Functions や Dataflow に逃がす設計が重要です。
  2. ローカルテスト・デバッグの難易度

    • 課題
      Cloud Composer (Airflow) はローカル環境を構築可能なので DAG のテストが可能ですが、Workflows はクラウド上のリソースと密結合しているため、ローカルでの完全な再現・テストが困難です。「修正してデプロイして実行」のサイクルになりがちです。
    • 対応
      ステップごとの単体テストは諦め、結合テスト中心で進める割り切りが必要でした。 また、別の Workflows に分割することで、ステップごとに運用できるように対応しました。
  3. ステップ間のデータ受け渡し制限(メモリサイズ)

    • 課題
      Workflows は大規模なデータをステップ間で直接受け渡すこと(ペイロードサイズ制限)には向いていません。
    • 対応
      今回の設計では、画像データそのものや大量のリストは Workflows 上を通過させず、必ず GCS のパスや BigQuery のテーブル名といった「参照情報」のみを受け渡すように徹底しました。
  4. ベンダーロックイン

    • 課題
      Airflow はOSS標準ですが、Workflows は Google Cloud 固有のサービスです。将来的に他のクラウドへ移行する場合、ポータビリティがありません。
    • 対応
      今回は GCP 完結のシステムであり、フルマネージドの恩恵(管理レス)を最優先しました。

6. まとめ

Workflows + Cloud Scheduler の組み合わせにより、日次の Vector Search インデックス更新を完全自動化できました。

コスト面以外では、インフラ管理コスト(Cloud Composer の環境維持など)を削減し、本質的なロジック開発に集中できることが Workflows の大きなメリットに感じました。

デメリットでYAML記法に言及しましたが、逆に言えば、どなたでも気軽に試してみることができるとも言えます。コストも軽いのでこれを機に是非一度お試しください。

読者の皆様がこれで良い体験を得ることができましたら私としても大変嬉しく思います。

明日18日目はAIテクノロジーグループの吉田さんです。