TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

385

Swift 6から本格的に導入された Strict Concurrency Checking は、アプリの安定性を飛躍的に向上させる一方、既存のコードの移行や、並行処理を初めて学ぶ開発者にとってはハードルが高いという課題がありました。 この課題に対応するため、Swift 6.2では「Approachable Concurrency」というビジョンが掲げられ、その中核機能として「Default Actor Isolation」が導入されました。 参考資料 この記事はWWDC 2025の以下のセッションを参考にしています。 Swiftの並行処理の活用 Code Along:Swiftの並行処理によるアプリの強化 Approachable Concurrencyとは? これは、開発者がデータ競合のリスクを最小限に抑えつつ、必要な時にだけ段階的に並行処理を導入できるようにするための、Swiftの設計思想や機能群全体を指す言葉です。 このアプローチは、以下の3つのフェーズを想定しています。 シンプルなシングルスレッドのコードを書く : 並行処理について明示的に指定しない限り、全ての処理がメインスレッドで動作するコードになります。 データ競合のない非同期コードを書く : UIをブロックしてアプリが固まるなど問題があれば、async/awaitなどを使い、安全に非同期処理を導入します。 並列処理でパフォーマンスを向上させる : アプリのパフォーマンスをさらに高める必要が出てきたら、TaskGroupやasync letなどを使い、複数の処理を並列で実行します。 並行性を段階的に導入することで、すべての開発者が最初から複雑な並行処理について深く理解する必要がないようにしています。 Default Actor Isolationとは? これは「Approachable Concurrency」を実現するための新機能で、Xcode 26から作成する新規プロジェクトではデフォルトで有効になります。 概要 : この設定が有効な場合、コードの大部分がデフォルトでメインアクタ @MainActor 上で実行されるように隔離されます。これにより、UIの更新などメインスレッドで行うべき処理が、意図せずバックグラウンドスレッドからアクセスされてクラッシュする、といった典型的なデータ競合を防ぎます。 既存プロジェクトでの設定 : 既存のプロジェクトでは、Xcodeのビルド設定から Swift Compiler - Language にある Default Actor Isolation の項目を MainActor に変更することで、この機能を有効にできます。 オプトアウト(分離の方法) : もちろん、重い処理をバックグラウンドで実行したいケースはあります。その場合は、関数や型に対して nonisolated キーワードや、Swift 6.2で導入された @concurrent 属性を明示的に指定することで、メインアクタから処理を切り離し、バックグラウンドで実行させることができます。 Swift 6.2での主な変更点 Default Actor Isolationをサポートするために、async関数の振る舞いがより直感的になるよう、いくつかの重要な変更が加えられました。 nonisolated async関数の振る舞いの変更 Swift 6.1まで : nonisolated な非同期関数は、呼び出し元のアクタに関わらず、常にバックグラウンドのスレッドプールで実行されていました。 Swift 6.2から : デフォルトで、呼び出し元の実行コンテキスト(アクター)を継承するようになります。つまり、 @MainActor から呼び出せば、その nonisolated async 関数も @MainActor 上で実行されます。これにより、意図しないスレッドの切り替えが減り、コードの振る舞いが予測しやすくなります。 新しい属性 @concurrent の導入 「 nonisolated だけど、この処理は意図的にバックグラウンドで実行したい」という場合は、新しく導入された @concurrent 属性を関数に付与します。 これにより、Swift 6.1までの「常にバックグラウンドで実行する」という振る舞いを明示的に選択できます。 既存プロジェクトのSwift 6移行 Swift 5で開発中のプロジェクトをSwift 6に移行する場合、Swift 6.2をターゲットにすることで移行が簡単になる可能性があります。 移行プロジェクトの場合、 Approachable Concurrency = No 、 Default Actor Isolation = nonisolated が選択されています。 これを、 Approachable Concurrency = Yes 、 Default Actor Isolation = MainActor に変更することで、新規プロジェクトと同様に段階的に並行処理を導入できるようになります。 既存の考え方は、並行処理を基本としつつメインアクターへの隔離や、データ競合を避けるための制約や属性を付与することによって Strict Concurrency Checking に適合させるアプローチでした。 新しい考え方では、メインアクターでのシングルスレッド処理を基本としながら、必要な箇所にのみ段階的に並行処理を導入するアプローチに変わります。Approachable Concurrencyを採用することで、既存のコードの多くをそのまま活用しながら、段階的に並行処理を導入し、安全にパフォーマンスを向上させることができる可能性があります。 まとめ Swift 6.2のDefault Actor IsolationとApproachable Concurrencyは、並行処理をより安全で、学習しやすくするための変更です。デフォルトでメインスレッドに処理を限定することで開発者をデータ競合から守りつつ、 @concurrent のような新しい制御方法を提供することで、必要な箇所では意図的にパフォーマンスを最適化する手段を提供しています。 弊社で開発している「トモニテ」アプリは現在Swift 5で実装されており、できるだけ早くSwift 6に移行したいと考えています。しかし、Strict Concurrency Checking への対応が課題になっています。 そこで、Swift 6.2をターゲットとし、Approachable Concurrencyのアプローチを採用することで、従来よりも少ない変更量で安全に Strict Concurrency Checking に対応できる可能性があると考えています。今後、この手法を用いた移行の検証を行う予定です。
アバター
ヘルシカ、デリッシュキッチンで Android アプリの開発を担当している岡田です。 時代の流れは早いもので、日々の開発業務で AI のサポートを受けることが当たり前になってきましたね。 今回は Android Studio Narwhal Feature Drop Canary 4 以降に Android Studio Gemini の Agent Mode がついに追加されましたので、 Gemini の rules 設定と合わせて見ていきたいと思います。 この記事が、皆さんの開発体験を向上させる一助となれば幸いです。 Agent Mode とは 機能単位での実装 高度なリファクタリング 複雑なエラーの根本解決 Agent Mode を使った実装の例 rules 設定について rules の Scope について IDE 設定(グローバルルール) Project 設定(Project 固有ルール) 各設定の優先度 例) IDE 設定と Project 設定の優先度 rulesに何を書くべきか IDE 設定の rules Project 設定の rules まとめ Agent Mode とは まず、今回の目玉機能である「 Agent Mode 」から詳しく見ていきましょう。 これまでの Android Studio Gemini は、私たちが書いたコードの一部を選択して「この処理をリファクタリングして」と依頼したり、 チャットウィンドウで「〇〇の実装方法を教えて」と質問したりする、いわば「指示待ち」の優秀なアシスタントでした。 この機能は Chat Mode として残っています。 新しく登場した Agent Mode は、その名の通り「エージェント(代理人)」として、より能動的かつ自律的に動作します。 Google Developers 公式ドキュメントでは、以下の通りに紹介されています。 developer.android.com Android Studio のエージェント モードの Gemini は、Gemini とのチャットだけではできない、複雑なマルチステージ開発タスクを処理するように設計されています。 大まかな目標を記述すると、エージェントが計画を作成して実行し、必要なツールを呼び出して複数のファイルに変更を加え、バグを反復的に修正します。 このエージェント支援ワークフローにより、複雑な課題に取り組んで開発プロセスを加速できます。 複数回のやり取りを通じて、Project 全体の構造やコードベース、依存関係、さらには設計思想まで深くコンテキストを理解し、ファイルやモジュールを横断して、より文脈に沿った的確なサポートを提供してくれるのです。 これまで複数の手順に分かれていた複雑なタスクを、一度の指示で実行できるようになります。 機能単位での実装 「アプリのホーム画面に、トピックのリストに移動する "フォロー" という新しいボタンを追加して」といった自然言語での指示から、必要な ViewModel、Composable 関数、Navigation の設定、さらには Unit Test の雛形まで、複数のファイルにまたがって生成・修正を提案してくれます。 高度なリファクタリング 「 <composable name> コンポーザブルで、 <modifier name> 修飾子のパディングを減らしてください」といったリファクタリング作業も、コード差分を明示しながら行なってくれます。 複雑なエラーの根本解決 「Project のビルドエラーを修正」などのプロンプトを使用してエージェントにビルドエラーの修正を依頼すると、推奨される修正が適用され、Project がビルドされてソリューションが検証され、問題が解決するまで反復処理されます。 根本原因を推測し、より堅牢な設計への改善案まで提示してくれます。 他にも Google Developers 公式ドキュメントにて、ユースケースについて記載されていますので、合わせてご覧いただけると幸いです。 developer.android.com Agent Mode を使った実装の例 例として、テスト用の Project へカレンダー画面を作成してみましたので、その記録を以下に示します。 まずは Agent Mode を選択し、カレンダー画面を作成する旨を伝えます。 この時、 @MainActivity と記載することで、明示的に MainActivity をコンテキストとして指定しています。 Gemini が CalendarScreen.kt というファイルに CalendarScreen という名の Composable 関数の作成案を提示してくれました。 ここで左右の矢印アイコン、Open in Diff View を選択すると、Diff View を見ることが可能です。 選択肢には Accept Change , Reject Change , Auto-Approve の 3 つがあります。 Accept Change はこの提示された変更を許可します。今回の場合は Diff View で明示された変更が実行されます。 Reject Change はこの提示された変更を拒否します。 Auto-Approve はこの後に提示される変更を自動許可します。変更履歴は Gemini に常に表示されますが、ユーザの確認無しに一気に作業が実行されます。 今回は Auto-Approve を選択しました。その後、数回のエラー・警告解消のやり取りを通して出力されたものが以下になります。 デザインを見ると、確からしいものが出来ています。 正直人間がコードを見るとリファクタしたくなると思いますが、0-1 を行ってくれるのは大幅な作業短縮になる場合があります。 また今後、コーディング作業の大半を Gemini に任せるのであれば、もしかしたらリファクタリングは必要ないのかもしれません。 これはあくまで個人の感想ですが、AI にコーディングを任せる上で、「ある程度のブラックボックス化されたコードを黙認すること」に人間が慣れなければいけないのかもしれないなと思っています。 まだ全てを Agent に任せることは難しいですが、簡単なリファクタなどから仕事を任せてみてはいかがでしょうか。 rules 設定について Agent Mode の能力を最大限に引き出すために不可欠なのが、 rules (ルール) の設定です。 これまでは、Gemini に対して毎回「日本語で回答して」、「Compose で書いて」などといった前提条件を伝える必要があり手間でした。 しかしこの rules を設定することで、前提条件をコンテキストとして Gemini に恒久的に教え込めます。 この機能は Android Studio の Settings > Tools > Gemini > Prompt Library から設定できます。 rules の Scope について Gemini の rules は、IDE 全体に適用される設定と、特定の Project のみに適用される設定の 2 つのレベルで Scope を管理できます。 IDE 設定(グローバルルール) どの Project を開いても適用される、開発者個人の好みや、組織全体で共有したい普遍的なルールを設定します。 Project 設定(Project 固有ルール) その Project に特化したルールや制約、背景情報を定義します。チーム開発での利用が主目的です。 各設定の優先度 基本的には、Project 固有のルールが IDE のグローバルルールよりも優先されます。以下はその例です。 例) IDE 設定と Project 設定の優先度 IDE 設定の rules のみに「Always respond in 日本語 with utf-8 encoding.」と記述した場合、日本語で出力してくれます。 上記に加え、Project 設定の rules に「Always respond in English.」と記述した場合の出力は英語となります。 rules に何を書くべきか IDE 設定の rules 基本的には Project 設定の rules が優先されるため、本当に汎用的なものを記載するのが良いと思います。 「Always respond in 日本語 with utf-8 encoding.」 Project 設定の rules Project 設定の rules には、Project の憲法となるような、具体的で明確な指示を記述することになると思います。 1. コーディング規約・スタイルガイド 「コメントは日本語で記述すること」 「Jetpack Compose の UI は、Google の公式マテリアル3デザインガイドラインに従うこと」 「マジックナンバー(説明のない数値リテラル)は使用せず、 companion object 内で定数として定義すること」 2. 技術スタック(使用ライブラリやアーキテクチャ)の指定 「アーキテクチャは MVVM を採用する」 「状態管理は ViewModel と StateFlow を使用する。LiveData は新規に採用しない」 「DI(Dependency Injection)には Hilt を使用する。Koin は使用しない」 「非同期処理は Kotlin Coroutines を使い、RxJava は使用しない」 「画像読み込みには Coil ライブラリを使用する」 3. Project の背景や目的 「このアプリは、IT 初心者向けの学習アプリです。コメントやドキュメントは、専門用語を避け、平易な言葉で記述してください」 「ターゲットユーザーは高齢者です。そのため、フォントサイズは大きめに設定し、クリック可能な領域も広く取るようなUIを心がけてください」 4. 禁止事項やアンチパターン 「Activity や Fragment 内で直接ネットワーク通信や DB アクセスを行わず、必ず Repository 層を介して行うこと」 「XML レイアウトでは findViewById は使用せず、常に ViewBinding を使用すること」 「Composable 関数内で重い処理を実行しないこと。必要であれば rememberCoroutineScope や副作用 API を適切に使用する」 これらのルールは、使用感や Project の成長に合わせてチームで議論し、変更していくことが重要だと思います。 例えば、「Gemini を使用している中で MVP アーキテクチャで回答が提示されたら、ルールに "アーキテクチャは MVVM を採用する" を追記する」といったフローで改善してくことになると思います。 弊社も rules は試行錯誤中ですが、記載内容を更新する度に求める回答に近づいている良い感覚があります。 まとめ 今回は Android Studioに搭載された Agent Mode と rules 設定について紹介させていただきました。 まずは最新の Android Studio Preview を導入し、簡単な rules からでも設定していただけると幸いです。 Gemini を「優秀なアシスタント」から「信頼できる開発パートナー」へと育て上げ、次世代の開発体験を楽しみましょう!
アバター
タイトル 株式会社エブリーでCTOを務めている今井( @imakei )です。 今回は、弊社で2ヶ月前に導入したCursorの成果についてお話しします。 結論から言うと、Pull Request数が2倍に増加するという、予想を上回る成果が出ています。 Cursorの導入とその背景 弊社エブリーは「明るい変化の積み重なる暮らしを、誰にでも」をパーパスに掲げ、レシピ動画メディア「デリッシュキッチン」などのサービスを展開しています。AIファーストカンパニーとして、プロダクトでのAI活用はもちろん、開発現場でもAIの実用的な活用を進めています。 その中で、下記のような価値実現を目指し、Cursorを導入しました。 本質的な価値創造への集中 AIによる高度なコーディング支援によって実装にかかる時間を短縮することで、エンジニアおよびPdMがより創造的で本質的な業務に注力できる環境を構築。より早く高品質なサービスが提供できるようになることで、ユーザー体験の向上を加速する。 プロダクト開発に関わる業務全体の生産性向上 エブリー開発部では、開発生産性10倍を目標としており、「Cursor」の導入でこれら全ての業務における効率化を促進し、組織全体の生産性向上を図る。開発生産性とは、単にプログラムを実装するだけでなく、機能のアイデア創出、企画立案、仕様策定、テストなど多岐にわたる業務を含む、プロダクト開発全般に対する生産性の向上を目指す。 AI活用を前提とした組織文化の醸成とイノベーションの推進 今後、あらゆるプロダクトにおいてAIの活用が不可欠になると予測される中、エブリーでは、日々の業務でAIに触れる機会を創出することで、AIを当たり前に使いこなす組織文化を醸成。これにより、AIを活用した新たな価値創造を主導し、イノベーションを推進する。 数値が示す明確な変化:Pull Request数が2倍に増加 導入から2ヶ月が経過し、GitHubにて1人あたりのPull Request数が 約2倍 に増加しました。 エンジニア一人当たりのPR数の推移 上記は、エンジニア一人当たりのPR数ですが、Cursor導入以降、かなりの勢いでPR数が増えてるのがわかると思います。 細かい部分でCursorのrulesのためのPR数も含まれていたり、6月以降はDevinも導入していたりするので、 PRの種類や質など、厳密には比較できない部分もありますが、それを差し引いても大きく伸びたと言えそうです。 この数字の背景を自分なりに分析してみました。 1. 純粋な開発速度の向上. 「30分かかっていたユニットテストの作成が5分で完了するようになった」 このような声を頻繁に聞くようになりました。 決まったタスクについては、AIによって開発速度が大幅に向上したといえます。 2. AIを活用していくという雰囲気・文化の醸成 会社としてCursorの導入を決めたことで、AIを活用していく雰囲気が組織全体に醸成されています。 今までは、AIを活用していても個人レベルにとどまっていましたが、Cursor導入後は積極的に知見を共有するようになり、AI活用のレベルが組織全体で少しずつ向上してきました。 また最近では、有志でAI活用の勉強会を開催するなど、AI活用がより活発になってきています。 3. ドキュメントの整備 AI活用を進める中で予想外の効果として、ドキュメントの整備が進んでいます。 AIと効果的に協働するには、暗黙知になっていたコンテキストをいかにAIに伝えるかが重要になってきます。それに気づいたチームから、自発的にドキュメントの整備が始まっています。プロジェクトの背景、設計意図、実装の詳細など、これまで口頭で共有されていた情報のドキュメント化が進んでいます。 これは単にAI活用のためだけでなく、新メンバーのオンボーディング改善にもつながっています。AIに説明できるレベルで文書化することで、人間にとってもわかりやすいドキュメントが生まれたのです。 開発生産性10倍を目指して 現在、私たちは「開発生産性10倍」という野心的な目標を掲げています。これはコーディングアシストだけでなく、要件定義や仕様策定といった上流工程にもAIを活用することで実現しようとしています。 今後は、以下のような領域からまずはAI活用を進めていく予定です: 要件定義・仕様策定 :ユーザーストーリーの整理や仕様書の下書き作成 テスト設計 :テストケースの網羅的な洗い出しと自動生成 プロジェクト管理 :工数見積もりやリスク分析の支援 コードレビュー :より高度な設計レビューとベストプラクティス提案 「開発生産性10倍」は決して簡単な目標ではありませんが、AI活用によってエンジニアがより創造的で本質的な業務に集中できる環境を作り続けていきたいと思います。 生成AIでプロダクト開発をアップデートしたい方、 生成AIを活用したプロダクトを作りたい方、 ぜひ弊社で一緒に働きましょう!
アバター
去年に引き続き、エブリーは2025年6月28日(土)に大田区産業プラザPiOで開催されたPHPカンファレンス2025に参加させていただきました。 今回も参加レポートとして、会場の様子やセッションの感想についてお届けします! イベント概要 https://phpcon.php.gr.jp/2025/ PHPカンファレンスは、PHP関連の技術を主とした技術者カンファレンスです。 2000年に日本のユーザ会によってPHPカンファレンスが初めて行われ、今年で26回目の開催となります。 これからPHPをはじめる方から、さらにPHPを極めていきたい方まで幅広く楽しめるイベントになるよう様々なプログラムをご用意しております。 セッションの感想 PHPの今とこれから2025 〜30周年を迎えたPHPと最新動向〜 2025年、PHPはついに誕生から30周年を迎えました。そして、PHPカンファレンスも今年で第25回。節目の年にふさわしく、「PHPの今とこれから」をテーマに、歴史と最新動向を振り返るセッションが展開されました。 その内容を簡単にお伝えできればと思います。 PHPの誕生とその時代背景 PHPが誕生したのは1995年。当時のインターネット黎明期には、いくつもの革新的な技術が登場しています。 1993年4月30日:CERNがWWWを無償公開 1994年12月15日:Netscape Navigator(初の一般向けWebブラウザ)リリース 1995年1月21日:Apache HTTP Server誕生 1995年5月23日:MySQL 1.0 リリース 1995年6月8日:PHP 1.0 登場 つまり、Webそのものの始まりと共にPHPも誕生したと言えます。初期はPerlの代替のような簡易スクリプトとしてスタートしたPHPですが、 その後の5.x系の成熟、7.x系の大幅な高速化などを経て、現在の堅牢で高機能な言語へと進化してきました。 PHPの現状と普及率 現在のPHPは、サーバーサイド言語として依然として圧倒的なシェア約74%に上るようです。 バージョンの利用状況を見ても、7.x系と8.x系がそれぞれ約4割ずつと拮抗していますが、 依然として古いバージョンを使い続けているシステムも存在していました。 そして特に印象的だったのが、最新のPHP 8.4の利用率がまだ1%未満という点。 多くのシステムがまだバージョンアップに慎重な姿勢をとっていることがうかがえます。 11月20日リリース予定のPHP 8.5 2025年11月20日に正式リリース予定のPHP 8.5の新機能にもフォーカスが当てられました。 パフォーマンス 8.4とほぼ同等のパフォーマンスですが、環境によっては7%ほど改善されている例もあるようです。 主な新機能(一部抜粋) パイプ演算子 シェルのように値を次々にパイプ「|>」で渡して処理できる構文。コードの可読性と柔軟性が向上。 WHATWG URL・RFC3986準拠のURI生成 セキュリティや整合性を強化する目的で、標準仕様に沿ったURL/URIオブジェクトを生成可能に。 クローン時にプロパティを指定可能:clone($x, $options) NoDiscardアトリビュート 呼び出し結果を無視すべきでないことを明示。 ライフサイクルとバージョンアップの重要性 PHPは現在、年1回のメジャーバージョンアップサイクルを採用しており、サポート期間は4年(バグ修正2年+セキュリティ修正2年)とされています。 最新機能を利用するためだけでなく、セキュリティサポートを継続的に受けるためにも定期的なバージョンアップは不可欠です。 2025年のPHPカンファレンスは、バージョンアップのお話だけでなく、今までの歴史や最新のシェア状況などのお話もあり大変面白いセッションでした。 PHP初心者セッション2025 〜ChatGPTと学ぶ、新時代のPHP入門〜 今年のPHPカンファレンスでは、初心者向けのセッション「ChatGPTと学ぶPHP入門」が開催されました。 開催前に挙手によるアンケートを行ったのですが、 参加者の8割がPHP初心者、中にはプログラム初心者もいるということで、これからPHPを始める人の多さに驚かされました。 このセッションのゴールは、ChatGPTを活用しながら、PHPで簡単なWebアプリを作ること。 はじめにPHPの基本文法や環境構築、変数や配列、条件分岐などの基礎が説明され、後半ではCSV出力のミニアプリをChatGPTと一緒に作成していきました。 印象的だったのは、「AIは主役ではなく相棒。開発を加速させる壁打ち役」という考え方。 エラー調査では「何をしたか」「どんなエラーが出たか」「どう解決したいか」をセットでAIに伝えると、より正確な回答が得られるというテクニックも紹介されました。 また、マニュアルを読むよりもAIに初心者向けに説明させる方が理解しやすい場面も多いとのこと。 セキュリティチェックやコードレビューにもAIは活用でき、今後はAIを使いこなすスキル自体が重要になっていくという話もありました。 AIと共に学ぶ新しいPHPの入口として、非常に実践的かつ未来を感じさせるセッションでした。 スポンサーブースの紹介 エブリーでは、 デリッシュキッチン に代表される様々なサービスを開発・運用しております。 その中でも小売業者向けのデータ連携サービス retail HUB では開発・運用にPHPを活用しています。 retail HUBは、小売業のDXを推進するサービスです。詳細は こちら をご覧ください! PHPを活用する中で多くの恩恵を受けている私たちも、コミュニティのさらなる盛り上がりに貢献するため、スポンサーとして協賛させていただき、ブースを出展しました。 「なぜエブリーが協賛しているのか?」については、こちらのブログ記事もご覧ください! PHP Conference Japan 2025 にGOLDスポンサーとして今年も協賛します! ブース エブリーでは、今回も弊社が提供する デリッシュキッチン のサービスをイメージしたブースの雰囲気を作りました。 多くの方にデリッシュキッチンを知っていると言っていただけてとても嬉しいと思いました。 デリッシュキッチンは知っているけど、エブリーという会社名は知らないという方も数多くいらっしゃいました。 今回のブース出展を機により多くの方にエブリーという会社名も認知していただければ嬉しいです! ノベルティ 今回もデリッシュキッチンにちなんだノベルティを用意させていただきました。 ステッカー CTOブレンドのコーヒーバッグ デリッシュキッチンお料理グッズ デリッシュキッチンお料理グッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。 当選した方の中には「めちゃめちゃ料理するのでたくさん使います!」と言っていただけてこちらも嬉しい気持ちになりました。 アンケート 今回、アンケートでは『使用しているAIツール』について回答していただきました。 GitHub CopilotとChatGPTが最も投票数を集める結果となりました。 稟議の関係でGitHub Copilotの導入が最もスムーズに進んだという方、様々なAIツールを並行利用して現在評価中という方など、 会社によって使っているAIツールは様々であることがわかり、とても興味深いアンケートとなりました。 ちなみにエブリーでは全エンジニアおよびプロダクトマネージャーが「Cursor」を導入しています。 詳しくはこちらの記事をご覧ください。 エブリー、AIエディタ「Cursor」を全エンジニアおよびプロダクトマネージャーに導入 ご回答いただいた皆様、ご協力いただきまして本当にありがとうございました! 各社スポンサーブースの様子 今年も各社ともに個性あふれるブースを展開しており、会場には活気が溢れていました。 クイズやアンケートパネル、さらにはコードレビュー体験など、多彩な企画が用意されていて、来場者が楽しみながら参加できる雰囲気でした。 スタンプラリー企画 まとめ PHPカンファレンス2025の運営の皆様、そしてご参加された皆様、今年も本当にありがとうございました! 昨年の開催から半年しか経っていないにもかかわらず、PHP 8.5の最新情報や今後の進化の話題も多く、毎年確実に進化を続けているPHPに、改めてワクワクしました。 また、これまではPHP歴の長い方がメインの参加者なのかと思っていましたが、「初心者向け」と書かれているセッションでは、「PHPどころかプログラミング自体が初めてです!」という方も多く参加されていて、本当に幅広い層が集う、まさに“初心者から上級者まで”楽しめる素晴らしいコミュニティだと実感しました。 私たちも今回得られた知見を活かし、PHP 8.5の新機能を活用したプロジェクトにもどんどん取り組んでいきたいと思います。 今後もこうしたイベントや勉強会に積極的に参加し、PHPコミュニティの一員として、引き続きPHPの進化を追いかけていきます!
アバター
はじめに こんにちは。 開発本部 開発1部 デリッシュリサーチチームでデータエンジニアをしている吉田です。 エブリーではサイネージ端末を利用した広告配信サービスを提供しており、サイネージ端末からのログを収集しています。 従来はTreasureData-SDKを利用して端末からTreasureData上のテーブルにログを送信していましたが、ログ収集基盤を移行することになりました。 今回、AWSのマネージドサービスを活用して、スケーラブルなログ収集基盤を構築したので、その内容を紹介します。 背景 従来のログ収集基盤では、TreasureData-SDKを利用して端末からTreasureData上のテーブルにログを送信していました。 これはコストや処理の面で非常に便利でしたが、ログの送信がSDKに依存しているため、仮にTreasureData以外のログ収集基盤へ移行する場合には、アプリケーションのコードを変更する必要がありました。 サイネージ端末の都合上、アプリケーションのアップデートを極力行わずにログ収集基盤を変更できるようにしたい、という要望がありました。 そこで、アプリケーションの改修をすることなく、バックエンドをいつでも自由に変更できる、より柔軟なログ収集基盤の構築を目指しました。 アーキテクチャ 新しく構築したログ収集基盤は以下のコンポーネントで構成されています。 API Gateway: アプリケーションからのログ送信用エンドポイント SQS: ログメッセージをバッファリング Lambda: SQSからメッセージを取得し、処理を行う Firehose: 処理されたログをS3に配信 S3: ログの長期保存 この構成により、以下のメリットを実現しています: スケーラビリティ: 突発的な大量のログにも対応可能 信頼性: SQS によるメッセージの永続化とデッドレターキューによる失敗処理 運用負荷の軽減: マネージドサービスの活用 全体のアーキテクチャは以下のようになります。 アーキテクチャ コスト面では、リクエスト数に応じて課金されるAPI Gatewayの料金が、この基盤全体の大部分を占めることになります。 しかし今回は、そのコストよりも、将来的にログ収集基盤を柔軟に変更できるというメリットを重視し、この構成を採用しました。 詳細 API Gateway API Gatewayはアプリケーションからログを受け取るエンドポイントを提供するとともに、SQSにメッセージを送信する役割を担います。 API Gateway から SQS へのインテグレーションでは、マッピングテンプレートを使用して、受信したJSONペイロードを SQS メッセージ形式に変換します。 terraformのコードは以下のようになります。 resource "aws_api_gateway_integration" "logs_post_integration" { rest_api_id = aws_api_gateway_rest_api.this.id resource_id = aws_api_gateway_resource.logs.id http_method = aws_api_gateway_method.logs_post.http_method integration_http_method = "POST" credentials = aws_iam_role.api_gateway.arn type = "AWS" uri = "arn:aws:apigateway:$ { var.aws_region } :sqs:path/$ { var.aws_account_id } /$ { aws_sqs_queue.primary.name } " request_parameters = { "integration.request.header.Content-Type" = "'application/x-www-form-urlencoded'" } request_templates = { "application/json" = "Action=SendMessage&MessageBody=$util.urlEncode($input.body)" } passthrough_behavior = "WHEN_NO_TEMPLATES" } SQS SQSはログメッセージをバッファリングし、Lambda関数による処理を待ちます。 API GatewayとLambdaの間にSQSを挟むことで、Lambdaの処理が失敗した場合でもメッセージを保持し、後で再処理できるようになります。 また、SQSのデッドレターキューを設定し、指定回数処理に失敗したメッセージを別のキューへ送信して処理させることで、エラーの確認を容易にするとともに、ログの喪失を防ぎます。 terraformのコードは以下のようになります。 resource "aws_sqs_queue" "dead_letter" {} resource "aws_sqs_queue" "primary" { name = "$ { var.prefix } -primary-queue" delay_seconds = 0 # 配信遅延秒 visibility_timeout_seconds = 30 # メッセージの可視性タイムアウト秒 max_message_size = 262144 # 256KB message_retention_seconds = 3 * 24 * 60 * 60 # メッセージの保持期間 (最大14日) receive_wait_time_seconds = 20 # メッセージ受信待機時間秒 sqs_managed_sse_enabled = true redrive_policy = jsonencode ( { deadLetterTargetArn = aws_sqs_queue.dead_letter.arn maxReceiveCount = 3 # メッセージがDLQに移動するまでの最大受信回数 } ) } resource "aws_sqs_queue_redrive_allow_policy" "this" { queue_url = aws_sqs_queue.dead_letter.id redrive_allow_policy = jsonencode ( { redrivePermission = "byQueue" , sourceQueueArns = [ aws_sqs_queue.primary.arn ] } ) } redrive_policyは、メッセージがデッドレターキューに移動する条件を設定します。 今回は最大3回の受信失敗でデッドレターキューに移動するように設定しています。 Lambda Lambda関数はSQSからメッセージを取得し、処理を行います。 トリガーとしてSQSを設定し、メッセージがキューに追加されると自動的に起動します。 terraformのコードは以下のようになります。 resource "aws_lambda_function" "primary" { function_name = "$ { var.prefix } -log-sender" role = aws_iam_role.lambda.arn filename = data.archive_file.dummy.output_path architectures = [ "arm64" ] handler = "main.lambda_handler" runtime = "python3.13" memory_size = 128 # MB timeout = 5 # 秒 logging_config { log_format = "JSON" system_log_level = "WARN" application_log_level = "INFO" } lifecycle { # 環境変数の変更を無視する # 別リポジトリで管理 ignore_changes = [ environment, ] } } resource "aws_lambda_event_source_mapping" "primary" { event_source_arn = aws_sqs_queue.primary.arn function_name = aws_lambda_function.primary.arn enabled = false batch_size = 1 maximum_batching_window_in_seconds = 0 function_response_types = [ "ReportBatchItemFailures" ] scaling_config { maximum_concurrency = 500 } lifecycle { ignore_changes = [ # enabledを無視する # コードデプロイ後に手動で有効化する enabled, ] } } aws_lambda_event_source_mappingのfunction_response_typesで ReportBatchItemFailures を指定しているため、Lambda関数で処理に失敗したメッセージだけがSQSに戻されます。 上記のTerraformではbatch_size=1と設定しているため、この設定は現状では効果がありませんが、将来的にbatch_sizeを変更する可能性を考慮して、このように設定しています。 Data Firehose FirehoseはLambdaからデータを受け取り、S3に配信します。 Lambdaから直接S3に書き込む場合、S3のAPI呼び出しコストやマイクロファイル(小さなファイル)が多数生成されるといった問題が発生します。 特に、マイクロファイルが大量に存在すると、後段でデータ処理をする際のパフォーマンスが著しく低下する原因となります。 Firehoseを利用するとバッファリング、データの圧縮、パーティショニング、S3への書き込みなどを自動で行ってくれるため、これらの問題を解決できます。 terraformのコードは以下のようになります。 resource "aws_kinesis_firehose_delivery_stream" "primary" { name = "$ { var.prefix } -primary-stream" destination = "extended_s3" extended_s3_configuration { bucket_arn = aws_s3_bucket.this.arn role_arn = aws_iam_role.firehose.arn buffering_size = 128 # MB buffering_interval = 300 # 秒 file_extension = ".json" # プレフィックスの設定 prefix = "logs/!{partitionKeyFromQuery:event_name}/!{partitionKeyFromQuery:year}/!{partitionKeyFromQuery:month}/!{partitionKeyFromQuery:day}/!{partitionKeyFromQuery:hour}/" error_output_prefix = "errors/!{firehose:error-output-type}/" dynamic_partitioning_configuration { enabled = true } processing_configuration { enabled = true processors { type = "MetadataExtraction" parameters { parameter_name = "JsonParsingEngine" parameter_value = "JQ-1.6" } parameters { parameter_name = "MetadataExtractionQuery" parameter_value = "{year:.time | gmtime | .[0] | tostring,month:.time | gmtime | (.[1] + 1) | (. | if . < 10 then \"0\" + tostring else tostring end),day:.time | gmtime | .[2] | (. | if . < 10 then \"0\" + tostring else tostring end),hour:.time | gmtime | .[3] | (. | if . < 10 then \"0\" + tostring else tostring end),event_name:.event_name}" } } } cloudwatch_logging_options { enabled = true log_group_name = aws_cloudwatch_log_group.firehose_primary.name log_stream_name = aws_cloudwatch_log_stream.firehose_primary.name } s3_backup_mode = "Disabled" } server_side_encryption { enabled = true key_type = "AWS_OWNED_CMK" } depends_on = [ aws_iam_role_policy_attachment.firehose ] } 動的パーティショニングの設定により、ログ中に含まれるイベント名と時間を元にS3のプレフィックスを動的に生成します。 MetadataExtractionQueryでは、JQを利用してJSON形式のログデータからパーティション分割に必要なキー(イベント名、年、月、日、時)を抽出しています。 これにより、後続のETLなどで効率的にデータを処理できるようになります。 S3 S3はFirehoseによって配信されたログデータの長期保存先として機能します。 バージョニングを設定して意図しない削除に備えるとともに、ACLをプライベートに設定して外部からのアクセスを制限します。 Lambdaの実装 Lambda関数では、SQSから受け取ったメッセージを処理し、Firehoseに送信します。 以下に実装の一部を示します。 def lambda_handler (event: Dict[ str , Any], context: Any) -> Dict[ str , Any]: batch_item_failures: List[Dict[ str , str ]] = [] record: Dict[ str , Any] for record in event.get( "Records" , []): message_id: Optional[ str ] = record.get( "messageId" ) try : message_body_str: Optional[ str ] = record.get( "body" ) log_data: Dict[ str , Any] = json.loads(message_body_str) # Send to Firehose firehose_success: bool = send_to_firehose(log_data) # その他の送信 if not firehose_success: batch_item_failures.append({ "itemIdentifier" : message_id}) except json.JSONDecodeError as e: batch_item_failures.append({ "itemIdentifier" : message_id}) continue except Exception as e: batch_item_failures.append({ "itemIdentifier" : message_id}) continue if batch_item_failures: return { "batchItemFailures" : batch_item_failures} else : return { "batchItemFailures" : []} 前述のLambdaのterraformコードで function_response_types = ["ReportBatchItemFailures"] としているため、関数の戻り値は {"batchItemFailures": [失敗したメッセージID]} とします。 これにより、複数メッセージを受け取った際に、失敗したメッセージのみをSQSに戻せます。 エラーとなるメッセージがない場合は、 {"batchItemFailures": []} を返します。 Lambdaのデプロイ Lambdaのコードデプロイには lambroll を利用しています。 コードと環境変数を管理するリポジトリと、Terraformでインフラを管理するリポジトリを分離しています。 これにより、インフラの変更とアプリケーションコードの変更のライフサイクルを分け、それぞれが独立して安全にデプロイできる体制を整えています。 lambroll init --function-name {function-name} --download --profile {aws-profile}で既存のLambdaのコードと設定ファイルをダウンロードできます。 このプロジェクトでは、function.dev.jsonのようにfunction.{env}.jsonという形式で環境ごとの設定ファイルを管理しています。 コードやfunction.jsonの編集後、 lambroll deploy --function=function.{env}.json --profile {aws-profile} でデプロイを行います。 まとめ 今回、AWSのマネージドサービスを活用して、スケーラブルなログ収集基盤を構築しました。 API Gatewayを利用することで、アプリケーションのアップデートを行うことなく、ログ収集基盤を変更できる柔軟性を持たせています。
アバター
はじめに こんにちは、エブリーでサーバーサイドをメインに担当している清水です。 私のチームではPHP, Laravelを使用して小売店向けのSaaS型Webサービスの開発を行っています。インフラはAmazon ECS (Fargate)です。 このシステムではユーザーのアクションを分析するためのイベントログを出力する機能があり、こちらをAmazon S3に出力するためにfluentbitを使用することにしました。 当初の予定では1日程度で終わるはず思っていたのですが、思った以上にうまくいかず1週間ほどかかってしまいました。 本記事では、Laravel → 標準出力 → Fluent Bit → S3 → Athenaという構成で、イベントログを出力するまでのフローをコードレベルで丸ごと紹介しつつ、いくつかハマったポイントを共有します。 本記事を参考にとりあえず動く状態にしていただいて、足りない部分はカスタマイズしていただくような形で利用いただければ幸いです。 環境情報 PHP 8.3.22 Laravel 11.18.1 Fluent Bit 1.9.10 Laravelからイベントログを標準出力する機能を実装する ある処理が呼び出された時のイベントログ出力処理例 public function show(int $id) { Log::channel('event')->info('記事閲覧イベントログ', [ 'event_type' => 'show_article', 'article_id' => $id, ]); // ※ログ以外の処理は省略 } イベントログ用のフォーマッターを実装 <?php namespace App\Logging; use Monolog\Formatter\NormalizerFormatter; use Monolog\LogRecord; class EventLogFormatter extends NormalizerFormatter { public function format(LogRecord $record): string { $formatted = parent::format($record); $segments = [ 'level' => $formatted['level_name'], 'datetime' => $formatted['datetime'], 'env' => $formatted['channel'], 'message' => $formatted['message'], 'extra' => $formatted['extra'], 'log_type' => 'EVENT', ]; // コンテキストのすべてのキーを出力に含める foreach ($formatted['context'] as $key => $value) { $segments[$key] = $value; } return json_encode($segments, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES).PHP_EOL; } } ※以下の記事を参考にさせていただきました。 Laravel(monolog)で構造化ログを実装する config/logging.phpにeventチャネルを定義 'event' => [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, 'formatter' => EventLogFormatter::class, 'with' => [ // Swooleを使用しているとstdoutの標準出力に余計な文字列がつけられるため、stderrとしました。 'stream' => 'php://stderr', ], ], ローカル開発環境にて標準出力でイベントログがJSON形式で出力されることを確認 {"level":"INFO","datetime":"2025-06-24T12:59:29.427634+09:00","env":"local","message":"記事閲覧イベントログ","extra":[],"log_type":"EVENT","event_type":"show_article","article_id":1} fluentbitを設定する Dockerfile # ベースイメージとしてAWS公式Fluent Bitイメージを指定 FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:stable # 作業ディレクトリの作成 WORKDIR /fluent-bit/etc # カスタムfluent-bit.confをイメージ内へコピー COPY fluent-bit-custom.conf /fluent-bit/etc/fluent-bit-custom.conf # カスタムパーサーファイルもコピー COPY json-string.conf /fluent-bit/etc/json-string.conf fluent-bit-custom.conf [SERVICE] Parsers_File /fluent-bit/etc/json-string.conf Flush 5 Grace 30 # 空のログをフィルタリング [FILTER] Name grep Match *-firelens-* Exclude log ^$ # JSONパース用フィルター:カスタムパーサー json_string を使用 [FILTER] Name parser Match *-firelens-* Parser json_string Key_Name log Preserve_Key Off Reserve_Data Off # event log のタグ付け [FILTER] Name rewrite_tag Match *-firelens-* Rule log_type ^EVENT$ s3.event.$event_type true # EVENTログ出力 [OUTPUT] Name s3 Match s3.event.* bucket ${AWS_S3_LOG_BUCKET} # 環境変数で出力先S3バケット名を指定する形とする region ap-northeast-1 use_put_object Off upload_timeout 60s compression gzip s3_key_format /EVENT/$TAG[2]/%Y/%m/%d/%H_%M_%S_$UUID.gz json-string.conf [PARSER] Name json_string Format json Time_Key datetime Time_Keep On Time_Format %Y-%m-%dT%H:%M:%S.%L%z →Dockerイメージをビルドして、latestタグでAmazon ECRにプッシュしておく ECSタスク定義でfluentbitを使用する設定に変更する "containerDefinitions": [ { "name": "{Webサービスの名前}", "image": "{イメージ名}", (省略) "logConfiguration": { "logDriver": "awsfirelens" } }, { "name": "log_router", "image": "XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/{fluentbitイメージ名}:latest", (省略) "environment": [ { "name": "AWS_S3_LOG_BUCKET", "value": "{出力先となるS3バケット}" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "{log_routerのログ出力先}", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "ecs", "awslogs-create-group": "true", "mode": "non-blocking", "max-buffer-size": "25m" } }, "firelensConfiguration": { "type": "fluentbit", "options": { "config-file-type": "file", "config-file-value": "/fluent-bit/etc/fluent-bit-custom.conf" } } } Athenaで外部テーブルを作成する CREATE EXTERNAL TABLE IF NOT EXISTS show_article_event_logs ( level string, datetime string, env string, message string, extra array<string>, log_type string, event_type string, article_id int ) PARTITIONED BY ( year string, month string, day string ) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' LOCATION 's3://{バケット名}/EVENT/show_article/' TBLPROPERTIES ( 'projection.enabled' = 'true', 'projection.year.type' = 'integer', 'projection.year.range' = '2025,2100', 'projection.year.digits' = '4', 'projection.month.type' = 'integer', 'projection.month.range' = '1,12', 'projection.month.digits' = '2', 'projection.day.type' = 'integer', 'projection.day.range' = '1,31', 'projection.day.digits' = '2', 'storage.location.template'= 's3://{バケット名}/EVENT/show_article/${year}/${month}/${day}/' ); 引っかかった部分 ローカルでテストする方法がわからなかった 最初は一発で上手くいくだろうと楽観視していきなりAWS上にデプロイしてテストしてみましたが、当然上手くいきませんでした。 ローカルでどうやってダミーデータを標準出力させるか?どうやってfluentbitにその標準出力を認識させるのか?といったことを考えていたら手が止まりました。 ローカルのテスト手順はこちらの記事の通りに対応して上手くいきました。 Fluent Bitを導入しました:ローカル実行・確認方法と、導入の過程でハマったこと application.conf にテストデータ投入用の[INPUT]、出力確認用の[OUTPUT]を追加します。 Name dummy Tag *-firelens-* Dummy {"date":"2022-01-23T03:10:33.317817Z","source":"stdout","log":"time:2022-01-23T03:10:33+00:00\tprotocol:HTTP/1.1\tstatus:200\tsize:1450\treqsize:150\treferer:-\treqtime:0.176\tcache:-\truntime:-\t"} Dockerを起動してターミナルを開き、以下でFluent Bitを起動します。 /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/application.conf コンテナ内でAWSのアクセスキーをexportするだけでS3へのアップロード処理も問題なく実行されます export AWS_ACCESS_KEY_ID="{AWSアクセスキー}" export AWS_SECRET_ACCESS_KEY="{AWSシークレットアクセスキー}" AthenaのSELECT実行結果が全カラム空白になってしまう 色々試した結果、ログファイルの拡張子が.gzになっていないと発生することがわかった # before s3_key_format /EVENT/$TAG[2]/%Y/%m/%d/%H_%M_%S_$UUID # after s3_key_format /EVENT/$TAG[2]/%Y/%m/%d/%H_%M_%S_$UUID.gz おわりに 最後までお読みいただきありがとうございました。 今回の作業をするにあたって多くの先駆者様の記事を参考にさせていただきました。 ログ出力する方法には様々な実装方法があり、どのような方法を採用するべきか悩みながら進めました。 この記事がこれから同じことをする人に少しでも参考になれば幸いです。
アバター
PHP Conference Japan 2025 にGOLDスポンサーとして今年も協賛します! はじめに PHPカンファレンス とは? 2024年のカンファレンス概況 2025開催プラン 昨年の様子など イベント当日について はじめに  この度、株式会社エブリーは、2025 年 6 月 28 日(土)に開催される「PHPカンファレンス2025」に、ゴールドスポンサーとして昨年に続き 今年も 協賛することになりました! phpcon.php.gr.jp PHPカンファレンス とは?  日本PHPユーザ会 PHPカンファレンス実行委員会が主催となって、2000年より年に一度開催されている日本最大のPHPのイベントです。  WEBサーバにインストールされているシェア8割を超える人気言語のイベントとして、初心者から上級者まで幅広い層のWEB系エンジニアが参加します。 2024年のカンファレンス概況 参加者数 約1,150名 協賛企業数 55社 スポンサーセッション数 7セッション セッション数 52セッション 2025開催プラン 会場 大田区産業プラザPiO オンラインツール 後日簡易アーカイブを実施予定 セッション 最大並行5トラック程度※トラックは増減する可能性はあります 25~35セッション程度+LTを予定 タイムテーブルは こちら コンテンツ ・基調講演 ・公募セッション ・スポンサーセッション ・スタンプラリー(プレゼント抽選) 懇親会 実施予定(有料) イベントの参加申し込みは こちら で行っておりますのでぜひ会場でお会いしましょう 昨年の様子など tech.every.tv tech.every.tv photos.app.goo.gl イベント当日について 当日は 大田区産業プラザPiOの1F大展示ホール会場 の各スポンサーブースにて、毎年恒例のスタンプラリーが実施されます。 エブリーのブースでは弊社のXアカウントをフォローしていただいた皆様全員に当たるくじ引き景品もございますので、ぜひご興味のある方はお越しください! ノベルティ 最後までお読みいただき、ありがとうございました!
アバター
こんにちは、 @きょー です!普段はデリッシュキッチン開発部のバックエンド中心で業務をしています。 はじめに OpenAPI で API 仕様書を書く際、 null 値を許容するプロパティの表現方法はバージョンによって異なります。たとえば、ユーザープロフィールのメールアドレスのように「値が存在しない( null )」を許容したいケースはよくありますが、その書き方や推奨される方法は OpenAPI のバージョンごとに変化してきました。 この記事では、OpenAPI 3.0.0 と 3.1.0 それぞれでの null 許容プロパティの書き方や、その背景、なぜ仕様が変わったのか、どちらを使うべきかについて解説します。API 設計やスキーマ管理で迷ったときの参考になれば幸いです。 OpenAPI 3.0.0 での nullable の基本的な使い方 OpenAPI 3.0.0 では nullable: true を使用することで、プロパティが null 値を許容することを表現できます。 components : schemas : User : type : object properties : id : type : integer format : int64 name : type : string email : type : string nullable : true # emailはnull値を許容 swagger で表示すると以下のようになります。 OpenAPI 3.1.0 での nullable の表現方法 OpenAPI 3.1.0 では、 nullable キーワードが廃止され、代わりに type 配列に null を含める形で表現するようになりました。 components : schemas : User : type : object properties : id : type : integer format : int64 name : type : string email : type : [ string, "null" ] # emailはnull値を許容 swagger で表示すると以下のようになります。 なぜ変更されたのか OpenAPI 3.1.0 での nullable の廃止には、重要な背景があります。この変更は OpenAPI Specification の Proposal で詳しく議論され、主に以下のような理由が挙げられています。 nullable キーワードの曖昧さ nullable: true は「型指定されたスキーマにおいて null を許容する」という意図で導入されたが、OpenAPI 3.0 のドキュメントではその意味や他キーワードとの相互作用が十分に明確化されていなかった。 nullable: false (デフォルト値)についても、 null を明示的に禁止するのか、単に変更しないのかが明確でなかった。 詳細: https://spec.openapis.org/oas/v3.0.3#schemaNullable JSON Schema との整合性 OpenAPI の nullable は「許容範囲を拡張する」性質(expanding assertion)を持っているが、 JSON Schema には存在しなく整合性がなかった。 詳細: https://json-schema.org/specification これらの曖昧さや、JSON Schema への整合性の無さにより、バリデーターの挙動に一貫性がありませんでした。そのため OpenAPI 3.1.0 では nullable が廃止され、type に null を指定する方法に変更されました。 コード生成への影響 自分のチームでは OpenAPI で定義したスキーマを元にコードを自動生成しているプロジェクトがあります。 oapi-codegen と openapi-typescript を用いて Go のモデル定義や TypeScript の型を自動生成しています。 OpenAPI のバージョンの違いによって変わる null 値の表現方法がツールによるコード生成にどう影響を与えるか確認してみようと思います。 TypeScript での生成コード openapi-typescript を使用して TypeScript のコードを生成しようとした場合、以下のようになります。 // OpenAPI 3.0.0 での生成コード export interface components { schemas : { User : { id : number ; name : string ; email : string | null ; } ; } ; } // OpenAPI 3.1.0 での生成コード export interface components { schemas : { User : { id : number ; name : string ; email : string | null ; } ; } ; } TypeScript の場合、生成されるコードに大きな違いはなく、 nullalbe として指定したプロパティは自動生成されたコードに反映されているのがわかるかと思います。 Go での生成コード oapi-codegen を使用して Go のコードを生成しようとした場合、以下のようになります。 // OpenAPI 3.0.0 での生成コード type User struct { Email * string `json:"email"` Id int `json:"id"` Name string `json:"name"` } // OpenAPI 3.1.0 での生成コード // WARNING: You are using an OpenAPI 3.1.x specification, which is not yet supported by oapi-codegen (https://github.com/oapi-codegen/oapi-codegen/issues/373) and so some functionality may not be available. Until oapi-codegen supports OpenAPI 3.1, it is recommended to downgrade your spec to 3.0.x Go の場合、OpenAPI 3.0.0 で生成されたコードは nullable として指定したプロパティが反映されているのがわかります。一方 OpenAPI 3.1.0 ではコードの自動生成がされず 3.0.0 にバージョンを落として実行するように警告が出されます。これは oapi-codegen が依存している kin-openapi で OpenAPI 3.1 系にはまだ対応していないためこのような挙動になっているとのことでした。 https://github.com/oapi-codegen/oapi-codegen/issues/373 https://github.com/getkin/kin-openapi/issues/230 ここでは取り扱いませんが こちら で言及されている OpenAPI Overlay を使って OpenAPI 3.1.0 を 3.0.0 に downgrade する手法もあるみたいです。 まとめ OpenAPI における null 値の表現方法について、仕様の違いとツールの対応状況を確認してきました。 仕様の違い OpenAPI 3.0.0 では nullable: true による独自拡張 OpenAPI 3.1.0 では type: [string, "null"] による JSON Schema 準拠 3.1.0 での変更は、より標準的な方法での null の表現を可能に 実際のツール対応状況 TypeScript (openapi-typescript) 3.0.0, 3.1.0 ともに問題なく動作 Go (oapi-codegen) 3.0.0 では正常に動作 3.1.0 は現時点で未対応(kin-openapi の制限) 3.1.0 を使用する場合は OpenAPI Overlay による downgrade が必要 新規プロジェクトで新しく API スキーマを書く場合、JSON Schema との互換性がある 3.1.0 の方式が望ましいものの、実際の採用にあたってはツールの対応状況を十分に確認する必要がありそうです。もし既存プロジェクトで 3.1.0 に移行する場合でも使っているツールの対応状況を把握してから移行するのが良さそうです。
アバター
目次 はじめに Liam ERD とは 主な特徴 サポート状況 導入 手順 1. Liam ERD のビルドをさせるための Dockerfile を用意する 2. Docker Compose ファイルを用意する 3. テーブル構成を出力するためのコマンドを用意する 4. テーブル構成を出力する 使い勝手 まとめ 最後に はじめに こんにちは、開発本部開発1部トモニテグループのエンジニアの rymiyamoto です。 プロダクトを開発していると、DB のテーブルは常に変化していきます。 そのため、テーブルの構成を可視化することで、テーブルの関係性を把握することができるので設計や開発の効率化に繋がります。 今回は、MySQL のテーブル構成を Liam ERD で可視化してみたので、その方法を紹介します。 Liam ERD とは Liam ERD は、データベースから ER 図を生成するツールで、複雑なスキーマを簡単に可視化することができます。 liambx.com 主な特徴 公式サイトで挙げられている、Liam ERD が選ばれる理由は以下の通りです。開発者にとって嬉しいポイントが詰まっています。 Beautiful UI & Interactive(美しく、インタラクティブな UI) 洗練されたクリーンなデザインで、どんなに複雑なデータベースでも直感的に理解できる ER 図を生成します。パン、ズーム、フィルタリングといった機能により、ストレスなく目的の情報にたどり着けます。 Simple Reverse Engineering(簡単なリバースエンジニアリング) 既存のデータベーススキーマを、明確で読みやすい ER 図にシームレスに変換します。これにより、プロジェクトの全体像把握や、新しいメンバーのキャッチアップが格段に容易になります。 Effortless Setup(手間いらずのセットアップ) 複雑な設定は一切不要です。データベースのスキーマ情報を提供するだけで、すぐに使い始めることができます。 High Performance(高いパフォーマンス) 小規模なプロジェクトから、100 を超えるテーブルを持つ大規模なプロジェクトまで対応できるように最適化されており、軽快な動作を実現しています。 Fully Open-Source(完全なオープンソース) Liam ERD はオープンソースソフトウェアです。誰でも自由に利用できるだけでなく、プロジェクトに貢献し、自身のニーズに合わせてツールを形成していくことが可能です。 サポート状況 2025/06/17 現在、全面的にサポートされているものとしては以下のものがあります。 liambx.com PostgreSQL Ruby on Rails Prisma tbls MySQL は直接はサポートされていませんが、tbls 経由で可能になっています。 tbls は、データベース スキーマを文書化するためのツールで、DB 構成をドキュメント化することができます。 現在 MySQL / SQLite / BigQuery は tbls 経由でサポートされています。 github.com . @k1LoW さんの協力によりtblsとの連携が実現しました! これによりMySQLやSQLite, BigQueryなど多くのデータベースでLiam ERDによるER図の可視化ができるようになりました 🙌 https://t.co/lu4BkAS7mJ — Hirotaka Miyagi (@MH4GF) 2025年1月29日 x.com 導入 今回はコンテナで動かしている MySQL のテーブル構成を出力したいため、CLI で実行してローカル環境で動かすことを想定しています。 サンプルを用意しているので、そちらを参考にしてください。 github.com 手順 1. Liam ERD のビルドをさせるための Dockerfile を用意する tbls のインストールは go install で行っています。 ※ go と tbls のバージョンは、執筆時点でのものです。 ARG GO_VERSION=1.24.4 FROM golang:${GO_VERSION}-alpine AS liam-builder RUN apk add --no-cache nodejs npm git bash ARG TBLS_VERSION=1.85.4 RUN go install github.com/k1LoW/tbls@v${TBLS_VERSION} WORKDIR /work 2. Docker Compose ファイルを用意する ER 図を生成する builder コンテナと、生成された ER 図を表示する viewer コンテナを用意します。 profiles を設定することで、docker compose up 実行時にこれらのコンテナが起動しないようにしています。 services: db: # 省略 # ER図をビルドするためのコンテナ builder: container_name: builder image: liam-erd-sample-builder:latest build: context: . dockerfile: ./dockers/liam/Dockerfile env_file: - .env volumes: - ./erd:/work working_dir: /work depends_on: - db profiles: - builder # 生成されたER図を表示するためのコンテナ viewer: container_name: viewer image: nginx:1.27-alpine volumes: - ./erd:/usr/share/nginx/html:ro ports: - "127.0.0.1:8080:80" profiles: - viewer 3. テーブル構成を出力するためのコマンドを用意する Makefile に一連の処理をまとめます。 open コマンドは macOS 用です。他の OS をお使いの場合は、コマンド実行後に手動でブラウザを開いてください。 .PHONY: view-erd view-erd: mkdir -p ./erd rm -rf ./erd/* docker compose run --rm builder sh -c "tbls out -t json -o schema.json \"mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:3306/${DB_SCHEMA}\" && npx --yes @liam-hq/cli erd build --format=tbls --input schema.json && mv dist/* ." docker compose --profile viewer up -d open http://localhost:8080 4. テーブル構成を出力する $ make view-erd 使い勝手 テーブル選択時に外部キー制約があると関連するテーブルと色付きで表示され視覚的にわかりやすく、動作も快適な印象を受けました。 また、テーブルの詳細はサイドバーで出てくるので確認がしやすく、テーブル名のみや全カラムを表示を選べるので、必要な情報を素早く確認することができます。 他にもリンクを生成する事ができるので、共有する際にも便利です。 まとめ 今回は、MySQL のテーブル構成を Liam ERD で可視化してみました。 Liam ERD は、データベースから ER 図を生成するツールで、複雑なスキーマを簡単に可視化することができます。 ツールとして導入難易度も低く、見た目や動作が良い印象を受けました。 また、最近出たオープンソースであるため、今後の発展性も期待できます。 まだチーム内で導入したばかりなので、今後の活用方法を模索していきたいと思います。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに エブリーでデリッシュキッチンの開発をしている本丸です。 4月に新卒の開発研修を行ったのですが、配属されたチームが異なることなどもあり開発環境が揃っていないという問題がありました。 anyenvやgoenvなどを設定してもらうという手もあったのですが、それ自体の設定にも手間がかかってしまうためDev Containerの設定を用意してその場は乗り切りました。 本記事では、Dev Containerの基本的な使い方から、Docker Composeとの連携、拡張機能の設定まで、実際の開発現場で役立つ知識をお届けできればと思っています。 Dev Containerとは Dev Containerは、開発環境をコンテナ化する仕組みです。Visual Studio CodeやCursorなどで利用することができ、開発に必要なツールやライブラリ、設定をすべてコンテナに封じ込めることができます。 主な利点 環境の統一 : チーム全員が同じ開発環境で作業できる 簡単に開発環境を作成できる : 新しいメンバーも数分で開発環境を構築可能 隔離性 : プロジェクトごとに独立した環境を持てる、AIエージェントを使う上で意図しない環境の変更を防げる 使い方 基本的な使い方 1. Dev Container設定ファイルの作成 プロジェクトルートに .devcontainer/devcontainer.json を作成します。 { " name ": " Go ", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile " image ": " mcr.microsoft.com/devcontainers/go:1-1.24-bookworm ", ... // Configure tool-specific properties. " customizations ": { // Configure properties specific to VS Code. " vscode ": { " settings ": {} , " extensions ": [ " streetsidesoftware.code-spell-checker " ] } } , ... // Use 'portsAttributes' to set default properties for specific forwarded ports. // More info: https://containers.dev/implementors/json_reference/#port-attributes " portsAttributes ": { " 9000 ": { " label ": " Hello Remote World ", " onAutoForward ": " notify " } } ... } 2. 開発環境の起動 devcontainer.json の作成が終わったらあとは開発環境のコンテナを起動するだけです。 VS CodeでDev Containers拡張機能をインストール コマンドパレット(Cmd+Shift+P)で「Dev Containers: Reopen in Container」を実行 これだけで、統一された開発環境が起動します。 サンプルコードで試すことができる 基本的な設定に関してはサンプルコードを参考にすることもできます。 サンプルコードを試す方法は簡単で コマンドパレット(Cmd+Shift+P)で「Dev Containers: Try a Dev Container Sample...」を実行 開発したい言語を指定する これだけで、簡単なサーバーを実行するためのサンプルコードを試すことができます。 ちなみに、「Dev Container設定ファイルの作成」で例示したものはサンプルコードのdevcontainer.jsonを引用したものです。 開発環境と同時にローカルの実行環境も立ち上げる 基本的な使い方では、イメージを指定して実行を行っていましたがDocker Composeファイルを指定して実行することもできます。 今回は、開発環境とローカルでの実行環境を同時に立ち上げるという形で説明させていただければと思います。 Dev Containerの対応前は下記で動いていたとします。 Go Docker Compose 1. Dev Container用のDocker Composeファイルを作成 今回は、元々のDocker Composeファイルをできるだけ変更することなく進めたかったため、Dev Containerの時だけ使用するDocker Composeファイルを用意することにしました。 プロジェクトルートの devcontainer ディレクトリに compose-devcontainer.yml を作成します。(命名はわかりやすいように変更しましたが、任意の名前で大丈夫なはずです。) services: dev-env: image: mcr.microsoft.com/devcontainers/go:1-1.24-bookworm volumes: - .:/workspace:cached ports: - "9000:9000" command: sleep infinity ここでは元々のDocker Composeで動かしていたserviceに加えて新しく dev-env を動作させています。 このdev-envがDev Containerでの開発環境にあたります。 2. devcontainer.jsonをDocker Composeに対応させる devcontainer.jsonをDocker Composeを使用するように変更していきます。 基本版からDocker Compose版への変更点 イメージ指定の変更 - "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm", + "dockerComposeFile": [ + "../docker-compose.yml", + "compose-devcontainer.yml" + ], image プロパティを削除して dockerComposeFile プロパティを追加します。この際、複数のYAMLファイルを選択することができます。 サービスとワークスペースの指定を追加 + "service": "dev-env", + "workspaceFolder": "/workspace", ここでは、どのサービスを開発環境として使うかなどの指定を行なっています。 変更後の完全なdevcontainer.json { " name ": " Go ", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile " dockerComposeFile ": [ " ../docker-compose.yml ", " compose-devcontainer.yml " ] , ... // Configure tool-specific properties. " customizations ": { // Configure properties specific to VS Code. " vscode ": { " settings ": {} , " extensions ": [ " streetsidesoftware.code-spell-checker " ] } } , " service ": " dev-env ", " workspaceFolder ": " /workspace ", ... // Use 'portsAttributes' to set default properties for specific forwarded ports. // More info: https://containers.dev/implementors/json_reference/#port-attributes " portsAttributes ": { " 9000 ": { " label ": " Hello Remote World ", " onAutoForward ": " notify " } } ... } 拡張機能に関して Dev Containerでの拡張機能の指定の方法は2つあります。 複数のユーザーで共通で使いたい場合は devcontainer.json に " customizations ": { // Configure properties specific to VS Code. " vscode ": { " settings ": {} , " extensions ": [ " streetsidesoftware.code-spell-checker ", ... , ] } }, を追加し、 extensions に任意の拡張機能を指定すれば大丈夫です。 一人のユーザーが複数のDev Containerの環境で同じ拡張機能を使いたい場合は settings.json に " dev.containers.defaultExtensions ": [ " streetsidesoftware.code-spell-checker ", ... , ] , を追加し、 dev.containers.defaultExtensions に任意の拡張機能を指定すれば大丈夫です。 まとめ Dev Containerについて調べたり、触ってみる中でDev Containerをどのように使うのかの具体的なイメージを掴むことができました。 まだ実際の開発の現場で利用はできていないため、実際のプロダクトを開発する際にも取り入れてみたいと思いました。 参考 https://code.visualstudio.com/docs/devcontainers/containers https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&ssr=false#overview https://www.cursor.com/ja/changelog/dev-containers
アバター
エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。 エブリーでは全社的に SSH を使ったサーバーへのログインから、AWS Systems Manager Session Manager ( 以下 Session Manager ) を使った運用に切り替えました。 tech.every.tv これは私達のチームで管理している他社クラウド (AWS 以外という意味で他社です) 上に存在するサーバについても対象ですが、Session Manager を直接利用することはできません。そこで Session Manager を使った SSH 踏み台サーバーを構築し、それ経由で SSH 接続することで、他社クラウド上のマシンの 22 番ポートを開放する範囲を限定することができました。 構成 簡単に前提とする構成を説明します。 AWS VPC と他社クラウドのネットワーク同士が Site-to-Site VPN で接続されている構成です。開発者や CI/CD は、AWS SSM を通して踏み台サーバーに SSH 接続し、更に踏み台サーバーから他社クラウド上の Target Server へ SSH でアクセスします。 この構成を取ることによって、目的のサーバーの SSH ポートの開放範囲を AWS VPC の CIDR ブロックのみに限定することができます。 Session Manager を通した SSH 接続 セッション開始時には aws ssm start-session というコマンドを使いますが、その際 AWS-StartSSHSession というドキュメントを指定することによって、SSH セッションが開始できます *1 。 なおその際の接続対象 (今回の場合、踏み台サーバー) では SSH 接続をサポートするように設定する必要があります *2 。つまり SSH サーバーである必要があります。 *1 : ステップ 8: (オプション) Session Manager を通して SSH 接続のアクセス許可を付与および制御する - AWS Systems Manager *2 : セッションを開始する - AWS Systems Manager 踏み台サーバーの構築 sshd を動かすシンプルなサーバーを作ります。今回 ECS タスクとして動作させますが、環境に合わせて EC2 や EKS などでも良いかと思います。 Dockerfile は下記のようになりました。シンプルですね。 FROM debian:stable-20250520-slim RUN apt-get update && \ apt-get -y upgrade && \ apt-get -y install openssh-server net-tools && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ mkdir /var/run/sshd COPY --chmod=755 entrypoint.sh /root/entrypoint.sh EXPOSE 22 ENTRYPOINT [ " /root/entrypoint.sh " ] 次に Dockerfile 内で ENTRYPOINT としている起動スクリプトです。 # entrypoint.sh #!/bin/bash if [ -n " $SSH_PUBLIC_HOST_KEY_BASE64ENCODED " ]; then echo " $SSH_PUBLIC_HOST_KEY_BASE64ENCODED " | base64 -d > /etc/ssh/ssh_host_ed25519_key.pub chmod 644 /etc/ssh/ssh_host_ed25519_key.pub fi if [ -n " $SSH_PRIVATE_HOST_KEY_BASE64ENCODED " ]; then echo " $SSH_PRIVATE_HOST_KEY_BASE64ENCODED " | base64 -d > /etc/ssh/ssh_host_ed25519_key chmod 600 /etc/ssh/ssh_host_ed25519_key fi echo " $ROOT_PUBLIC_SSH_KEY_BASE64ENCODED " | base64 -d > /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys exec /usr/sbin/sshd -D ホストキーが起動毎に毎回生成されるのを避けるため、タスク起動時にあらかじめ生成した鍵でファイルを作成しています。なお各キーは AWS Secrets Manager に Base64 エンコードし保管してあるものを、そのまま ECS タスクの環境変数として注入している都合でデコードしています。 運用上の工夫 目的のサーバーへ踏み台サーバーを経由し SSH 接続をするためには、SSM セッションを確立後に SSH での多段接続という手順を踏む必要があり、毎回行うのは手間です。そこで簡単なスクリプトを使って、接続時の手間を軽減しています。 まず ECS タスクへと aws ssm start-session コマンドを実行する場合、 --target オプションを <cluster_name>_<task_id>_<container_runtime_id> といった形式での指定が手間なので、下記 Make ターゲットで出力できるようにしています(中身はいささか力技ですが)。 CLUSTER_NAME := '<cluster_name>' FAMILY_NAME := '<family_name>' ACCOUNT_ID := `aws sts get-caller-identity --query "Account" --output text;` TASK_ID := `aws ecs describe-tasks --cluster $(CLUSTER_NAME) --tasks \`aws ecs list-tasks --cluster $(CLUSTER_NAME) --family $(FAMILY_NAME) | jq -r '.taskArns[0]'\` | jq -r '.tasks[0].taskArn' | awk -F'/' '{print $$3}'` CONTAINER_RUNTIME_ID := `aws ecs describe-tasks --cluster $(CLUSTER_NAME) --tasks \`aws ecs list-tasks --cluster $(CLUSTER_NAME) --family $(FAMILY_NAME) | jq -r '.taskArns[0]'\` | jq -r '.tasks[0].containers[0].runtimeId'` .PHONY: echo-as-ssm-target echo-as-ssm-target: @echo "ecs:$(CLUSTER_NAME)_$(TASK_ID)_$(CONTAINER_RUNTIME_ID)" .ssh/config は下記のようにしています。少々トリッキーですが、 make コマンドを .ssh/config 内で使用しています。 ProxyJump オプションにより、 bastion 経由で target_server への多段接続が自動的に行われます。結果的に ssh target_server を実行するだけで目的のサーバーへログインできます。 # .ssh/config host bastion User root IdentityFile ~/.ssh/<bastion_key_name> ProxyCommand sh -c "aws ssm start-session --target `make -C <path_to_makefile_dir> echo-as-ssm-target` --document-name AWS-StartSSHSession --parameters 'portNumber=%p'" Host target_server ProxyJump bastion IdentityFile ~/.ssh/<target_key_name> HostName ... GitHub Actions などの CI/CD でも同様に、適切な IAM Role を渡せば SSM セッションを開始し、SSH 経由でデプロイやコマンド実行が可能です。 まとめ 今回踏み台サーバーを構築し、Session Manager を通した SSH 接続を行うことで、他社クラウドのマシンにおいても SSH ポートの開放範囲を絞ることができ、また運用上の手間も減らすことができました。似たような環境の方のお役に立てばと思います。
アバター
はじめに こんにちは。デリッシュキッチンでデータサイエンティストをしている古濵です。 2025年5月27日〜30日に開催された 第39回人工知能学会全国大会(JSAI2025) に、プラチナスポンサーとして協賛いたしました。 今年は史上最多の参加者数を更新したようで、学会としての盛り上がりを肌で感じることができました。 tech.every.tv エブリーとしても人工知能学会への参加は今回が初めてでした。 本記事では、スポンサーブースでのエブリーのAIプロダクト開発に関する紹介や、聴講した講演等についてまとめていきたいと思います。 スポンサーブース 弊社のスポンサーブースでは「AIエージェントによるレシピ提案の開発と応用事例」というタイトルで、AIプロダクトであるデリッシュAIについて事例紹介を行いました。 多くの学生、大学教員、企業の方々と交流することができ、デリッシュキッチンがどのようなサービスなのかという基本的な部分から、デリッシュAIのプロダクトの詳細や今後の展望まで、幅広くお話しすることができました。 ブースでは、実際にデモ機を用いてデリッシュAIを体験していただきました。 中には、デリッシュキッチンを普段からご利用いただいている方や、現役の管理栄養士の方ともお話しする機会がありました。 デリッシュキッチンに「こういう機能があると良い」といった具体的なご要望も含めてお聞かせいただき、非常に有意義な時間を過ごすことができました。 お立ち寄りいただいた皆様、ありがとうございました。 聴講 人工知能学会では多くの講演やセッションが開催されており、個人的に聴講して特に印象に残ったものをご紹介したいと思います。 自ら話しかけるチャットボット実現のための話題選択RAGの提案 JSAI2025/Proposal of a Topic Selection RAG for Developing Proactive Chatbots ユーザーがチャットボットとの会話を継続する意欲を維持することの課題に焦点を当てた研究です。 既存の受動的なチャットボットとは異なり、ユーザーの興味や知識に基づいて能動的に新しい話題を提供できるチャットボットを提案しています。 ユーザーの興味を刺激する未知の情報を効率的に取得するために、話題をテーマごとにデータ化し、話題選択が可能なRAGシステムを構築している点が非常に興味深く感じました。 デリッシュAIでも、ユーザーのクエリに対してレシピを提案するだけでなく、今日何を作るか決まっていないユーザーとコミュニケーションを取りながらレシピ提案を行えるようになれば良いなと考えており、この研究のアプローチは大変参考になりました。 大規模視覚言語モデルチューニングを用いた非構造ドキュメント画像向け情報抽出 JSAI2025/Information Extraction from Unstructured Document Images with Tuned Large-Scale Vision-Language Model 非構造化文書画像に対する情報抽出手法について、複数のアプローチを比較検討した研究です。 OCR+LLMによる手法では、レイアウト要素が欠落してしまうため、視覚情報も考慮できるLVLM(Large Vision Language Model)を用いた比較検討を行っています。 具体的には、「LVLMによる手法におけるFew-shotとFine-tuningの比較」と「OCRの活用有無による比較」を実施していました。 結果として、LVLMをFine-tuningした手法が最も優れており、OCRを使用しない方が良い結果となったことは興味深い知見でした。 LVLMについては、別の講演で「大規模視覚言語モデルの開発」のチュートリアルも開催されており、画像とテキストの情報を統合的に処理できるLVLMの技術的発展を実感することができました。 LLMエージェントによるエルファロル・バー問題の解析 JSAI2025/Analysis of the El Farol Bar Problem by LLM Agents エルファロル・バー問題は、バーに行きたがっている複数の人が同時に意思決定を行う状況を想定した、ゲーム理論における問題です。 この発表では、各エージェントがLLMによって発話・記憶・行動を生成することで、エルファロル・バー問題をシミュレーションし、その結果を解析した研究について紹介されていました。 ゲーム理論の問題をLLMでシミュレーションするというアプローチが研究として非常に興味深く感じました。 シミュレーション結果では、エージェントがバーに入る前に何人かで集合したり、発話内にハッシュタグが出現してエージェントのグループ内で情報が拡散されたりするなど、個人的には予想外な行動パターンが観察され驚きました。 集団インタラクションを通じて個性を分化させるという、ある種の社会的な振る舞いが、エージェントによって実現可能であることを示唆する結果は、大変興味深いものでした。 おわりに 今回、人工知能学会(JSAI2025)にプラチナスポンサーとして参加し、エブリーのAIプロダクトであるデリッシュAIについて多くの方々にご紹介することができました。 学生から研究者、企業の方々まで幅広い層の皆様と交流でき、デリッシュキッチンやデリッシュAIに対する貴重なフィードバックをいただくことができました。 個人的には、学生時代がコロナ禍だったこともあり、学会の雰囲気と盛り上がりを直接体感することができて非常に良い経験となりました。 聴講した研究発表から得られた知見を、今後のプロダクト開発にどのように応用できるか模索していきたいと思います。 最後に、学会でお話しいただいた皆様、運営スタッフの皆様に心より感謝申し上げます。 来年の人工知能学会にも、さらに発展したプロダクトとともに参加できることを楽しみにしています。
アバター
Cursor v1.0 で追加された Memories を試してみる はじめに 注意 Memories とは Generate Memoriesの設定 実際に使ってみた 検証環境 実際のやりとり 完成! 🎯 主な機能 📁 プロジェクト構成 ✨ 特徴 🚀 使用方法 保存した知識 1. MCP Server 実装パターン(Golang) 2. MCP ツール定義のベストプラクティス 生成されたMemory 考察 まとめ はじめに こんにちは、TIMELINE 開発部 Service Development をしている hond です! タイトルにある通り、Cursorの0.51から待望のMemoriesがBETA機能としてリリースされました! リリースされたばかりということで ChangeLog にも詳細はないので、自分なりに使ってみた感想や調査結果を共有できればと思います! このblog執筆時点の2025/06/02時点では、ChangeLogにv0.51やGenerate Memoriesに関しての記述はありませんでしたがv1.0リリースにてChangeLogにv1.0の内容とともにMemoriesの内容が追加されました。 注意 2025/06/02現在、BETA機能としてリリースされたMemoriesですが、privacy modeが無効でないと使うことができません。 privacy modeを無効にした場合、どのような情報がCursorに共有され、自分の環境で使う際には問題はないのか確認の上利用することをお勧めします。 Memories とは 以前の Cursor Custom modes を利用した cursor-memory-bank のワークフローを試してみる でも取り上げましたが、メモリとはセッション間での命令内容やプロジェクトの知識を構造化したドキュメントとして保存する方法です。これによって新しいセッションを開始した時や、コンテキストウィンドウが肥大化した際にも、プロジェクトの詳細や技術スタック、アーキテクチャ、現在取り組んでいる内容やそれに関する進捗を繰り返し説明する必要をなくす一つの手段です。 Clineでは Blog や 公式ドキュメント で、Memory Bankを導入する方法が紹介されていましたが、Cursorではなかったので、これらの方法を参考に運用していた人も多いのではないでしょうか。ちなみに、Cursorの forum でもMemory Bank導入に関してはFeature Requestとして投稿されて、多くのユーザーが関心を寄せていました。 Generate Memoriesの設定 0.51で追加されたGenerate Memories機能はCursor Settings > Rulesから確認することができます。 Generate Memoriesの設定 冒頭でも説明した通り、privacy modeが無効でないと機能を有効化することはできません。 設定は以上になります! 実際に使ってみた 以降、実際に行った内容は こちら のRepositoryにアップロードしてあります。 検証環境 Cursor Version : 0.51.1 Model : claude-4-sonnet User Rules : なし Project Rules : なし Repositoryの状態 : gitの設定のみ Chat Mode : Agent 目的 : Goを用いた時刻を返すMCP Serverを作成させる 実際のやりとり 下記はMCP Serverの作成とMemoryを作成するにあたり、実際に行ったやりとりです。 User golang を用いて現在時刻や引数として渡された時間分経過した時の時刻を取得できる MCP server を作成して Cursor Golang を使用して時刻を取得できる MCP server を作成いたします。まず、プロジェクトの構造を確認してから実装を進めます。 Ran tool プロジェクトが初期状態のようですね。Golang MCP server を作成していきます。まず、必要なファイル構造を作成します。 MCP Go SDK が存在しないようですね。標準的な MCP プロトコルを実装するために、JSONRPC ベースの実装を作成します。 次に、README.md ファイルを作成して使用方法を説明します。 最後に、テスト用のサンプルスクリプトを作成します。 テストクライアントのファイル名を変更して、型エラーを修正します。 Ran tool 最後に、実際にサーバーが動作するかテストしてみましょう。 go mod tidy go build -o time-mcp main.go cd test && go run client.go 素晴らしい!MCP Time Server が正常に動作しています。最後にプロジェクトの構造を確認しましょう。 cd .. && find . -type f -name "*.go" -o -name "*.md" -o -name "go.mod" | sort 完成! Golang を使用した時刻取得機能を持つ MCP server を正常に作成しました。以下が実装された機能です: 🎯 主な機能 現在時刻取得 ( get_current_time ) 現在の時刻を様々なフォーマットで取得 デフォルトは RFC3339 形式 時間加算 ( add_minutes_to_current_time ) 現在時刻に指定された分数を加算(負の値で過去の時刻も取得可能) 様々なフォーマットに対応 📁 プロジェクト構成 main.go - MCP Time Server のメイン実装 go.mod - Go module 定義 README.md - 詳細な使用方法とドキュメント test/client.go - 動作確認用のテストクライアント time-mcp - ビルドされた実行可能ファイル ✨ 特徴 標準ライブラリのみ使用 - 外部依存関係なし 豊富な時刻フォーマット対応 - RFC3339、Kitchen、JST 等 20 種類以上 MCP 準拠 - Model Context Protocol 2024-11-05 に準拠 JSON-RPC 2.0 - 標準的なプロトコル実装 エラーハンドリング - 適切なエラーレスポンス 日本語対応 - 日本語でのメッセージ表示 🚀 使用方法 # ビルド go build -o time-mcp main.go # 実行 ./time-mcp # テスト cd test && go run client.go テスト結果からも分かるように、サーバーは正常に動作し、現在時刻の取得や時間の加算機能が期待通りに動作しています。MCP プロトコルに準拠しているため、MCP 対応のクライアントから利用することができます。 User ここまでの実装でメモリに保存する必要のある内容はある? Cursor はい、このプロジェクトの実装について、メモリに保存すべき重要な内容がいくつかあります。特に、MCP プロトコルの実装パターンや技術的な決定事項は今後の参考になります。 Ran tool Ran tool メモリに保存した内容は以下の通りです: 保存した知識 1. MCP Server 実装パターン(Golang) 標準ライブラリのみでの MCP サーバー実装方法 必須の MCP メソッド(initialize、tools/list、tools/call) JSON-RPC 2.0 準拠のレスポンス構造 適切なエラーハンドリング Go の時刻フォーマット活用法 2. MCP ツール定義のベストプラクティス JSON Schema を使った InputSchema の適切な定義 日本語対応のアプローチ 引数の柔軟性とデフォルト値設定 型安全な引数解析の実装 E2E テストの戦略 これらの知識は、今後 MCP サーバーを実装する際や、既存のサーバーを拡張する際に非常に有用です。特に、標準ライブラリのみでの実装パターンや、適切なエラーハンドリング、テスト戦略などは再利用可能な重要な知見です。 生成されたMemory 下記は実際にGenerate Memories機能によって生成されたUser Ruleになります。 Golang で MCP(Model Context Protocol)サーバーを実装する際の重要なパターン: 1. **標準ライブラリのみでの実装**: 外部依存関係なしで JSON-RPC 2.0 ベースの MCP サーバーを実装可能。bufio.Scanner + os.Stdin/Stdout で標準入出力通信を実現。 2. **必須の MCP メソッド**: initialize(プロトコルバージョン 2024-11-05、capabilities、serverInfo)、tools/list(利用可能ツール一覧)、tools/call(ツール実行)の 3 つが基本。 3. **レスポンス構造**: MCPResponse は必ず jsonrpc:"2.0"、id、result/error フィールドを含む。ツール実行結果は content 配列内に type:"text"と text フィールドで返す。 4. **エラーハンドリング**: JSON-RPC 標準エラーコード(-32601: Method not found, -32602: Invalid params)を使用。 5. **時刻フォーマット対応**: Go の time 定数(time.RFC3339、time.Kitchen 等)を活用し、カスタムフォーマット(JST 等)も追加可能。 考察 一回目のプロンプトでMCP Server自体の実装は完了していたので、そのタイミングでMemoryが自動で作成されると思ったのですが、作成されなかったので「ここまでの実装でメモリに保存する必要のある内容はある?」のように明示的にメモリに保存するように指示することでMemoryが作成されました。 実際のやりとり ではわかりづらいですが、「はい、このプロジェクトの実装について、メモリに保存すべき重要な内容がいくつかあります。特に、MCP プロトコルの実装パターンや技術的な決定事項は今後の参考になります。」の後の「Ran tool」でUser Rules updateが実行されて、User Rulesに書き込まれています。 ClineのMemory Bankの設定では大事だと思った部分をMemory Bankに保存するルールがあったため、一定明確な指示をしなくても保存してくれていたので、もしGenerate Memoriesでは明確な指示が必要な場合は少し使い勝手が悪そうだなと感じました。 また、設定の際に気づいた人もいるかと思いますが、今回のGenerate Memoriesで作成されるのはあくまでUser Rulesになります。そのため、今までプロジェクトレベルでカスタマイズしたルールを共有するなどのアプローチをとっていて、Generate Memoriesの結果を共有する場合は、一度自分で設定のUser Rulesを開いて、そこから今回取得された設定を別途ファイルに書き出す必要があります。 まとめ まだBETA版ではありますが、Memory Bank機能は個人的にも期待していて、ずっとforumを追っていたので、公式で導入されたのは嬉しい限りです! Memory Bankを実装したMCP Serverを使うアプローチもありますが、それを用いない場合、必要なプロジェクトで都度Memory Bankに関するルールを記述するかグローバルに設定する必要があったので、その設定がなくなるのは一つCursorの強みになるのかなと感じました。 また、Generate Memoriesが導入されたためか、User Rulesも複数設定できるようになり、管理が容易になった点も今回のアップデートの良い点だと思います。ルール設定などまだベストプラクティスが見つけられていないので、Generate Memoriesで作成されたものを採用しつつ、より開発効率を上げていきたいと思います。
アバター
はじめに こんにちは、トモニテで開発を担当している吉田です。 サービスを運営する上で、セキュリティ対策は欠かせません。 本記事では、実際にトモニテが受けた攻撃の事例をもとに、 異常検知から調査の経緯、攻撃の詳細、そして発見された問題点や今後の対応についてまとめています。 セキュリティリスク 現代の Web サービスにおいて、セキュリティリスクは多岐にわたります。代表的なものとしては、 クロスサイトスクリプティング(XSS) 、 クロスサイトリクエストフォージェリ(CSRF) 、 ブルートフォース攻撃 などが挙げられます。これらのリスクは、サービスの信頼性やユーザーの安全を脅かす重大な問題となり得ます。 中でも、SQL インジェクションは古くから存在する攻撃手法の 1 つで、昨年にはある企業の会員制サイト内の過去に使用していたページのセキュリティ設定に不備があり、同サイトのデータベースから顧客情報などが漏洩したといった事案も発生しました。( 参考 ) このように、SQL インジェクションは今なお多くのサービスで脅威となっています。ここからは、実際にトモニテで発生した SQL インジェクション攻撃の記録と、そこから得られた教訓について紹介します。 異常検知と調査の経緯 今回、調査のきっかけとなったのは、Web サービスのレスポンスタイムが長くなっているというアラート通知と、ほぼ同時にデータベースの CPU 使用率が高くなっているというアラート通知があったことでした。 実際にログを確認したところ、多くのリクエストに対しては HTTP ステータス 400 のエラーが返されていました。 しかし、「なぜ多くのリクエストが 400 エラーで弾かれているにもかかわらず、データベースの CPU 使用率が上がっているのか?」という点に疑問を持ちました。 このことから、「もしかしたら一部の攻撃的なリクエストがアプリケーションのバリデーションをすり抜けてデータベースまで到達しているのではないか」と考え、さらに詳細な調査を進めました。 その結果、一部のクエリパラメータに対してバリデーションが不十分であったことが判明し、これが攻撃リクエストに対して HTTP ステータス 200 を返してしまう原因となっていたことが分かりました。 では、実際にどのような攻撃が行われていたのか、具体的な内容を紹介します。 受けた攻撃の詳細 詳細な調査の結果、実際に受けた攻撃はクエリパラメータに SQL 文を埋め込むことで、データベースに対して不正な命令を実行させようとするものでした。 この攻撃の特徴は、パラメータに SQL インジェクションのペイロードを仕込むことで、データベースの動作を意図的に遅延させたり、条件分岐を利用して情報を引き出そうとする点です。 特に、DBMS_PIPE.RECEIVE_MESSAGE や sleep 関数など、タイムベースのブラインド SQL インジェクションが試みられていました。 実際にログに残っていたリクエスト例をいくつか挙げます。 1. tag_id にランダムな値を指定したリクエスト /path?tag_id=dn8r1je0elkm.html&page=1&per_page=20 推測される意図: 一見普通のリクエストに見えますが、tag_id にランダムな値(dn8r1je0elkm.html)が入っています。 これは「存在しない ID」や「想定外の値」を入れることで、アプリケーションのエラーハンドリングやレスポンスの違いを観察し、脆弱性の有無や内部構造を探ろうとしている可能性があります。 2. page パラメータに OR 条件を含めたリクエスト /path&page=-1 OR 2+138-138-1=0+0+0+1&per_page=20 推測される意図: page パラメータに「-1 OR 2+138-138-1=0+0+0+1」という SQL 文が埋め込まれています。 これは SQL インジェクションの典型的なテストで、「OR」条件を使って本来の条件式を無効化し、任意の条件でデータを取得できるかを試しています。 この場合、2+138-138-1=0+0+0+1 は常に真(1=1)になるため、全件取得や認証回避などが狙いと考えられます。 もしアプリケーションがこの注入を受け入れてクエリを実行した場合、通常はデータが返されないはずの状況(例: 存在しない page -1)でも、1=1 が真になることでデータが返されるといった挙動の変化が起こる可能性があります。 このリクエストは Web アプリケーションが SQL インジェクションに対して脆弱であるかどうかを探る、初期段階の偵察攻撃と考えられます。 3. Oracle の DBMS_PIPE.RECEIVE_MESSAGE を使ったリクエスト /path?page=1'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||'&per_page=20 推測される意図: page パラメータに「'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||'」という Oracle 特有の関数が使われています。 これは DBMS_PIPE.RECEIVE_MESSAGE を使って、SQL の実行を 15 秒間遅延させる(タイムベースのブラインド SQL インジェクション)攻撃です。 レスポンスの遅延時間によって、SQL インジェクションが成立しているかどうかを判別しようとしています。 4. MySQL の sleep 関数を使ったリクエスト /path?page=(select(0)from(select(sleep(15)))v)/'+(select(0)from(select(sleep(15)))v)+'"+(select(0)from(select(sleep(15)))v)+"/&per_page=20 推測される意図: page パラメータに「(select(0)from(select(sleep(15)))v)」など、 MySQL 系の sleep 関数 を使った SQL インジェクションが仕込まれています。スリープが正常に(中断なしで)復帰すると、0 が返されます。 これもタイムベースのブラインド SQL インジェクションで、SQL が実行されると 15 秒間レスポンスが遅れるため、脆弱性の有無を確認できます。 いずれも「SQL インジェクションが成立するかどうか」を探るためのテストや、タイムベースの手法で脆弱性の有無を確認しようとする攻撃です。 特に 3・4 番目は、データベースの種類(Oracle か MySQL か)によって使い分けている点も特徴的です。 これらのリクエストは、アプリケーションが入力値を適切に検証・サニタイズせずに SQL クエリを組み立てている場合、データベースに対して不正な操作を行うことができてしまう危険性を示しています。 攻撃の影響とシステムの挙動 調査の結果、基本的には不正なパラメータに対しては HTTP ステータス 400 を返していました。 しかし一部の攻撃リクエストに対して HTTP ステータス 200 が返っていたことがわかりました。 幸い、クエリのビルド時に正規のパラメータのみを受け付けていたため SQL インジェクション自体は成立していなかったことが判明しています。 ただし、攻撃者が仕込んだインジェクションペイロードがそのままアプリケーションに渡っていたことは事実です。 仮に SQL インジェクションが成立していた場合、データベース内の情報漏洩や、サービスの停止、データの改ざんなど重大な被害につながる恐れがありました。 特に、タイムベースのブラインド SQL インジェクションが成立していれば、攻撃者は時間をかけてデータベースの中身を窃取することも可能です。 発見された問題点と今後の対応 今回の調査で、一部のリクエストパラメータに対してバリデーションが適切に行われていなかったことが判明しました。 特に、攻撃リクエストの中には本来不正な値であるにもかかわらず、HTTP ステータス 200 を返してしまっていたものがありました。 これは、パラメータの値に対する十分な検証が行われていなかったことが原因です。 本来、許可された文字列のみを受け付けるべきですが、現状は任意の文字列がそのまま渡ってしまう状態でした。 この問題に対し、該当するパラメータに対して正規の値のみ受け付けるようホワイトリスト(許可リスト)によるバリデーションを追加し、不正な値が渡らないように修正を行っています。 まとめ 今回の事例を通じて、たとえ SQL インジェクションが実際に成立していなかったとしても、入力値のバリデーション不足は大きなリスクとなることを改めて認識しました。 また、調査の中で一部のパラメータに対するバリデーションが不十分だったことが、攻撃リクエストに対して HTTP ステータス 200 を返してしまう原因となっていたことも判明しました。 今後は、入力値に対して適切なバリデーションを徹底し、同様のリスクが他のパラメータにも潜んでいないか定期的に見直しを行っていきます。 セキュリティ対策は一度実施すれば終わりではなく、継続的な改善と運用が不可欠です。今後も気を緩めることなく、サービスの安全性向上に取り組んでいきます。
アバター
はじめに こんにちは、リテールハブ開発部でバックエンドエンジニアをしているホシと言います。 現在、小売アプリの開発でLaravel11を利用してAPI開発を行っています。 今回はとても便利で、開発効率を大きく上げてくれるツール「LaravelのEloquent ORM」についてお話できればと思います。 ただ、Eloquentに限った話ではなくORM全体の話でもあるのですが、使い方を間違えるとパフォーマンス低下や予期しないバグを引き起こすこともあります。 実際に使用してみて、便利さの裏にある注意点や、SQLの知識・理解が非常に重要であることを実感したので、今回はその点についてお話しできればと思います。 1. Eloquentとは? Laravelでのデータ操作をシンプルにするORM Eloquentは、Laravelに標準搭載されているORM(Object-Relational Mapping)です。 これを利用することで、データベースのテーブルをPHPのオブジェクトとして扱えるようになります。 通常、データベース操作を行うにはSQLを書く必要がありますが、Eloquentを使えばSQLを使用せず、PHPコードだけでデータの取得・更新・削除などができます。 たとえば、usersテーブルのID=1のnameデータを取得する場合、SQLの場合は以下のように書きます。 SELECT name FROM users WHERE id = 1 ; ただ、上記はデータベースから取得するためのSQLというだけで、 実際のコードでは、このSQLを実行した上で、結果からnameを取り出す処理を別途記述する必要があります。 Eloquentを使用すれば、上記の処理が含まれた状態でデータ取得できます。 その代わり、Eloquentはモデルの定義が事前に必要です。 このコードでID=1のユーザのnameを取得できます。 <?php $ users = User :: find ( 1 ) ; $ users -> name ; // ユーザ名の取得 Userはusersテーブルに対応する「モデル」クラスです。 モデルを通じて、EloquentはSQLを自動で生成・実行してくれます。 このモデルを使用することで、取得したデータも簡単にアクセスできます。 モデルとテーブルの関係 Laravelでは、モデル名とテーブル名は命名規則に従って自動的に対応してくれます。 あらかじめUserモデルを定義しておくことで、上記のようなEloquentを使用したデータ取得ができます。 モデル名:User 対応テーブル名:users また、Eloquentの強みの1つが「リレーション(テーブル間の関連)」を簡単に扱える点です。 モデル間のリレーションも簡単に定義することができます。 例えば、1人のユーザが複数の投稿(Post)を持つ「1対多」の関係を定義する場合: <?php // Userモデル public function posts () { return $ this -> hasMany ( Post :: class ) ; } と定義することで、ユーザの投稿一覧も簡単に取得できるようになります。 以下のコードでID=1のユーザの投稿内容を取得できます。 <?php $ user = User :: find ( 1 ) ; $ posts = $ user -> posts; SQLのようにJOINを意識せず、オブジェクトの形で関連データを取得できます。 このように、データの取得からアクセスまでを一貫して扱える点が、Eloquentの大きな利点です。 2. Eloquentの「落とし穴」:気付きにくいN+1問題とその回避法 前述の通り非常に簡単にデータ取得ができてしまい、様々なケースでも何となくの理解で私自身使用していました。 ただ、実際は裏でどのように動いているのかを理解して使用しないと思わぬ落とし穴があることがわかりました。 そこで、Eloquentの動きを理解せず使用していると特に遭遇しやすいN+1問題についてお話します。 N+1問題とは? 例えば、ユーザ一覧とそれぞれの投稿数を表示したいとします。 usersテーブルとpostsテーブルは1対多の関係で定義しているとします。 ユーザ毎の投稿数を表示するコード: <?php $ users = User :: all () ; foreach ( $ users as $ user ) { echo $ user -> posts -> count () ; } 一見シンプルで正しく動作しているように見えますが、実際にはパフォーマンスが大幅に低下する書き方です。 データ取得の流れとして、 最初にusersテーブルを1回検索 その後、各userに対して postsテーブルへクエリ(投稿数N回)を繰り返し検索 つまり、合計で1 + N回のクエリが実行されてしまいます。 例えばユーザが1000人いれば、クエリは合計で1001回実行されることになり、パフォーマンスに大きな影響を与えます。 開発時はデータ数も限られているため、特に遅くなるわけでもエラーになるわけでもなく正常に取得できるため、動きを理解していないと問題に気付きにくいです。 ORMにおける「Eager」と「Lazy」の考え方 Eloquent(ORM)でリレーションを扱うときに登場する重要な概念が、 「Eager Loading(イーガーローディング)」 と 「Lazy Loading(レイジーローディング)」 があります。 どちらもリレーション先のデータを取得する方法ですが、 パフォーマンスに大きな影響 を与えるため、使い分けが非常に重要です。 Lazy Loading(遅延読み込み) デフォルトでは、Eloquentはリレーションを「必要になったとき」に読み込みます。 これが Lazy(レイジー)=遅延 読み込みです。 この方法の大きなメリットは上述の通り必要な時に読み込むということになります。 例えばメインデータを取得時点では不要なケースで、ある操作や表示時などの必要な時だけに読み込むようにすれば、 余計な読み込みが減り、リソースを効率的に使用できます。 しかし、その代わり繰り返し取得しないといけないケースなどではパフォーマンスに大きな懸念があります。 Eager Loading(即時読み込み) 上記を回避するために使うのが、Eager Loadingです。 リレーションを最初からまとめて読み込む方法です。 コード例: 「with」を使用して関係テーブルを記述します。 <?php $ users = User :: with ( 'posts' ) -> get () ; foreach ( $ users as $ user ) { echo $ user -> posts -> count () ; // 追加クエリなしでアクセスできる } この場合、発行されるSQLは以下の2回のみになります。 SELECT * FROM users; SELECT * FROM posts WHERE user_id IN (...); →これがN+1問題の回避につながります。 ただし、Eager Loadingにも注意すべき点があります。 上記はwith指定があるため、Eager Loadingとして、 SELECT * FROM posts WHERE user_id IN (...); を一緒に実行して、1回の検索で必要なposts情報が取得できています。 しかし、このIN句に入るIDがどの程度の規模かをしっかり把握しておかないと予期せぬ大量データの事前取得につながります。 ここがSQLのJOINを使用した考え方と大きく異なるところかと思います。 もちろんJOINでも大量データの考慮は必要ですが、JOINの場合は1クエリで取得でき、より大量データ取得とのパフォーマンスは高くなります。 こちらの件はうまく使い分けることの重要性の話でもあり、後ほど説明できればと思います。 以下のSQLのように、 select * from users inner join posts on users.id = posts.user_id この結合であれば、仮に1万のusersデータがあっても問題なく取得はできますが、 Eager Loadingでは、user_id IN (...) のように複数IDを一括で取得するため、IN句に大量のIDが含まれる可能性があり、DBの制約、メモリ使用量や処理時間に影響を及ぼします。 この事象が起きることは常に考慮してEloquentを使用する必要があります。 N+1を避けるには? リレーションをループで使うときは必ずwith()の使用を検討します。 また、例えばusersにposts、postsにcommentsが関連している場合、正しくwithを指定します。 <?php User :: with ([ 'posts' , 'posts.comments' ]) -> get () ; とそれぞれ指定する必要があります。 これも理解していれば特に問題はないのですが、私自身「'posts.comments'」だけでどちらのテーブルも入っているので十分だと思っていた時期がありました。 普通に動くので問題があることがしばらく気付けず・・・。 また、当たり前ですがプログラム上で繰り返し取得するようなケースを書いている場合もN+1問題と同様の事象になるので、 そういったコードについても対策は必ず行う必要があります。 3. Eloquentを使用する場合もSQL知識、理解は非常に重要 データを取得する際、シンプルな取得であれば前述したような1行、2行で記載でき、特にSQLを意識する必要はあまりありませんが、 少し複雑な条件であったり、複数のテーブルをまとめて取得するようなケースでは、Eloquentのコードだけでは、どのようなSQLが生成・実行されているかを把握しづらいことがあります。 少ないデータやシンプルなテーブル構成というのは実際のサービスではほとんどないと思いますので、 コードを実装する上では結局どのようなSQLを使用してデータを取得しているのかをしっかりと把握する必要があります。 特に重要なSQL理解 INNER JOIN、LEFT or RIGHT JOINなどの結合仕様 リレーション先のテーブルに対してのWHERE条件のための結合やサブクエリの知識 今後のデータ数に合わせたパフォーマンス考慮、インデックス設定の検討 実行SQLを確認する方法(何となく取得できていそうを避ける) Eloquentを使っていると、裏でどんなSQLが発行されているか分かりづらいことがあるため、 開発時は常にどのようなSQLが発行されているかを以下のログ出力を利用して確認できるようにしておきます。 私の失敗例: <?php $ query = User :: where ( 'email' , 'like' , '%@example.com' ) ; dd ( $ query -> toSql ()) ; 上記は以下のSQLが返ってきます。 select * from `users` where `email` like ? しかし、実際の値が分からないことに加え、Eloquentでは単なるSQLの実行とは異なり、 モデル経由で値を取得する際に、Lazy Loadingによって上記以外の場所で意図せず追加のSQLが実行されているケースもあります。 ここで私は他にSQL発行していることに気づけず、どのタイミングでどのようにpostsのデータが取得できているのかをしばらく調べることになりました・・・。 正しい確認方法:DB::listen()を使用する <?php use Illuminate\Support\Facades\DB; DB :: listen ( function ( $ query ) { logger () -> info ( 'SQL実行ログ' , [ 'sql' => $ query -> sql, 'bindings' => $ query -> bindings, 'time' => $ query -> time . ' ms' , ]) ; }) ; ※このログ出力は開発環境のみに限定するなど制御することで、不要なログ蓄積やセキュリティリスクを防げます。 このコードをLaravelにあるAppServiceProviderのboot()メソッドに仕込むことで、アプリ全体のクエリをログで確認できます。 これにより、どのようなSQLが実行されているかをすべてログで確認することができるようになります。 なぜこのログ出力が大事か ログ設定を行った場合に実際に出力されるログ内容です。 [2025-05-XX 12:34:56] local.INFO: SQL実行ログ { sql: "select * from `users` where `email` like ?", bindings: ["%@example.com"], time: "1.22 ms" } このログ出力は、開発において非常に重要な情報源となります。 想定しているクエリが発行されているか確認できる。 パフォーマンスのボトルネックを見つけやすくなる。(実行時間、実行回数など) 中間処理やAPIレスポンスの生成時など、意図していなかった箇所でクエリが実行されていることがわかるようになる。 他にツールの設定などでN+1問題を見つける方法などもありますので、そういったものも併用するとより解消しやすいかと思います。 ただ、上記を行うだけでも格段にSQLの問題は見つけやすくなります。 これまでの内容を踏まえ、Laravelのデータ操作の最適な方法は? Laravelには2つの主要なデータベース操作手段があります: Eloquent :LaravelのORM。モデルベースで直感的・オブジェクト指向。 Query Builder :SQL構文ベース。柔軟で高速。 両者には得意不得意があり、 場面によって使い分けることが大切 です。 以下のようなケースでは無理にEloquentにこだわらず、状況によってはQuery Builderの使用も検討すべきです。 集計・統計クエリ(GROUP BY / COUNT / JOIN)を使用したデータ取得 大量データの取得、更新処理など サブクエリ・複雑なWHERE句を使用したデータ取得 モデルが不要な一時的なテーブル操作やJOINを使用したデータ取得 それぞれの補完関係を表にしてみました。 EloquentとQuery Builderの使い分け比較 比較項目 Eloquent Query Builder 可読性 ◎ ◯(SQLに近い) モデルの活用 ◎ ✕ 複雑なクエリ構造 △ ◎ パフォーマンス △(特に大量処理) ◎ 柔軟な構文制御 △ ◎ チーム開発との親和性 ◎(モデルベースで役割明確) ◯(要コメントや命名工夫) まとめ:Eloquentを安全に、賢く使うために いかがでしたでしょうか。 Eloquentは非常に便利ですが、SQL知識、理解があってこそ真価を発揮するのではないかと思います。 それぞれの特性を活かして、最適なデータ取得ができるようにしていきたいです。 また、実際に実行されるSQLを常に意識し、必要に応じてログを確認する習慣を持つことが重要だと思います。 特にAPI作成の上でパフォーマンスを意識するなら、SQLの知識も必須かと思います。 Eloquentの便利さを活かしながらも、裏側の仕組みや発行されるSQLを意識し、より安定した高パフォーマンスな開発ができるようにしていきたいです。 もしSQLをログで確認していない方は、ぜひログ設定をして実行されるSQLを確認していきましょう! 今回の記事が少しでも皆さんの開発のヒントになれば幸いです。 最後までお読みいただきありがとうございました。
アバター
TSKaigi 2025 に参加してきました! 目次 TSKaigi 2025 に参加してきました! はじめに イベントの様子 参加レポート The New Powerful ESLint Config with Type Safety AI Coding Agent Enablements in TypeScript - エージェントを自走させよう 複雑なフォームを継続的に開発していくための技術選定・設計・実装 TS特化Clineプログラミング まとめ 最後に はじめに Dev Enableチームの hond と庄司( ktanonymous )です。 2025年5月23日(金)、24日(土)の2日間に渡って開催されたTSKaigi 2025に参加してきましたので、イベントの様子や印象に残ったセッションをいくつかご紹介します。 各セッションのアーカイブも公開予定とのことですので、ぜひ公式サイト・YouTubeチャンネルなどをチェックしてみてください。 2025.tskaigi.org www.youtube.com イベントの様子 TSKaigi 2025は、昨年のいきおいをそのままに、2days開催に拡大してのイベント開催となりました。 TypeScriptを中心にしたカンファレンスとしてTypeScriptの最新情報や活用事例などが紹介され、コミュニティの盛り上がりも感じられました。 昨年と同じく、会場には多くのエンジニアが集まり非常に盛り上がっていました。 会場内では様々なブースが設けられ、各スポンサー企業でのTS活用事例などの紹介が行われていました。 オープニングの様子 株式会社KINTO様のブース 株式会社ドワンゴ様のブース 株式会社ドワンゴ様が 「 any 型を使うのはOK?」というタイトルでアンケートを実施されていました。 ボード上のグラフを突き抜けて回答している方もいて印象に残っていますが、そのほかにもアンケートやTypeScriptにちなんだクイズなどのコンテンツを提供されているブースも多く、どこも盛況の様子でした。 参加レポート The New Powerful ESLint Config with Type Safety 発表者: Anthony Fu さん レポート: hond talks.antfu.me この発表ではLegacy ConfigとFlat Configの比較や移行方法、管理・運用していく中でのノウハウが紹介されていました。 その中でも特に管理・運用に便利なツールが特に印象に残ったので紹介していきます。 ESLint Config Inspector ESLint Config InspectorはESLintの設定がどのように設定されているか視覚的に把握するためのツールです。どのようなルールがどのファイルに適用されているかを簡単に把握することができます。 eslint-flat-config-utils eslint-flat-config-utilsはFlat Configをより簡単に管理・操作するためのユーティリティ集となっています。複数の設定を結合したり、ルールの無効化などの操作、設定のチェインを行えるようにするためのComposerが提供されています。 eslint-typegen eslint-typegenはESLintのルールスキーマから型を自動作成するツールとなっています。これを用いることでエディタでルールオプションの自動補完や型チェックを行えるのでルールの記述が容易になります。 まだ弊社ではFlat Config対応できていない部分もあるのでこれらのツールを用いて適切に管理していきたいです。 AI Coding Agent Enablements in TypeScript - エージェントを自走させよう 発表者: Yuku Kotani さん ( https://yuku.dev/ ) レポート: 庄司 speakerdeck.com こちらのセッションでは、AIコーディングエージェントをより自律的に動かすことで、より開発者のタスクを減らしていくためのアプローチが紹介されました。 基本的な方針は、AIエージェントに探索させる「解空間を絞り込む」ということです。 これは、発表での表現をお借りすると、『「任意のTypeScript」くらいの広い解空間』から解(出力)を探索するのは精度が低くなるから、ドメインなどのプロジェクト固有の知識を与えることで探索するべき解空間を狭めようという考え方に基づいています。 解空間を絞り込むためのアプローチとして、以下のポイントが挙げられました。 コンテキストを注入する(e.g. cursor rules など) 「解空間の定義」を与える 機械的検査 出力を検査して、NGな出力にフィードバックして解空間に押し戻す また、具体的に取り組まれているアプローチとして、以下のようなものが紹介されました。 型情報を与えることで、解空間を絞り込む 静的解析や自動テストを組み込むことで、解空間を絞り込む デザインシステムを MCP サーバー化することで、解空間を絞り込む 生成AIは発展が著しく、いかに活用するかを模索中の方も多いかと思いますが、「解空間を絞り込む」という考え方を改めて言語化されたことは、とても納得感のある提案だと感じました。 また、30分で実装する開発者と30秒で実装するAIとでは、1分の静的解析に対するボトルネックの程度が段違いであるため、ツールチェインなどの速度もシビアになってくるという点も発表の中でも言及がありました。 そういった視点は自分自身はあまり意識できていませんでしたが、数字を見て、確かに両者の持つ時間スケールは変わってくるなと思い、とても印象に残りました。 複雑なフォームを継続的に開発していくための技術選定・設計・実装 発表者: izumin5210 さん ( https://x.com/izumin5210 ) レポート: 庄司 speakerdeck.com こちらのセッションでは、複雑で難しくなりがちなフォーム開発について、それに対する技術選定や設計・実装の工夫について解説されました。 フォームの開発は、入力値の管理や入力に基づく状態の構築、同期・非同期処理、そして最終的な出力まで、種々の工程が絡み合うため非常に複雑になりやすいという課題があります。 発表ではまず、react-hook-formのようなフォームの管理を便利にしてくれるフォームライブラリの使用することが紹介されました。 一方で、バリデーションのようなロジックなどがUIの記述に埋もれやすいというデメリットがあるため、複雑さに対処するためにも、「構造」や「ロジック」、「振る舞い」をしっかり分離してモデリングすることが重要だと言及されていました。 加えて、zod のようなバリデーションスキーマライブラリを活用することで、UIと制約の分離や管理に対して自然と注目できるようになるメリットがあります。さらに、不要な「状態」を持たせずに「値」として扱う設計にすることで、よりシンプルで保守しやすい実装が可能になることへの言及もありました。 解決したい課題によって適切なモデルは変わってくるため、プロジェクトに応じたモデリングや技術選定・設計が重要になってくると思いました。 例示するフォームが徐々に複雑なものに置き換えていきながら具体的にどういったアプローチが考えられるのかが紹介され、フォーム開発の難しさやそれに対するアプローチの考え方を再認識できたと思います。 弊社でも複雑なフォームの開発をしている領域があるため、非常に共感できる内容で、参考になりそうなものは積極的に導入していきたいと感じました。 TS特化Clineプログラミング 発表者: mizchi さん レポート: hond https://tskaigi.mizchi.workers.dev/ この発表では開発を行う中で実際にうまくいったプロンプト、うまくいかなかったプロンプトやそれらの考察が語られていました。 特に印象的だったものについて紹介していきます。 効くプロンプト: テスト駆動開発 (最重要) AI自身が壊れたことを早期に検知し自己修復するためにTDDは大事とのことです。TSKaigiの他の発表でもありましたがtest等を採用せず人間が評価する方針(ヒューマンインザループ)をとると人間の評価時間がボトルネックになるのでこの方針は大事だなと感じました。特にcommit粒度を高くしていれば壊れた時点で元の状態に戻しそこから改めて作業を再開することもできると思いました。 効くプロンプト: コメントによる自己記述 実装時にコードだけでなくそのコードの実装の概要をコメントで記述させることで修正を行う際に一貫性を持たせるとのことでした。個人的にも実装時と修正時で同じようなプロンプトを与えているがずれてしまうことを問題に感じていてメモリバンクを使ってAIが行った作業を思い出させようとしていましたが、コードと一対一で対応するわけではないので完全な一貫性を持たせることはできていませんでした。コメントをAIにかかせることで前述の通りAIに修正を行わせる際に一貫性が生まれるだけでなく、AIが作成したコードを人間が修正するときにもその助けになると感じました。 効く: URL を読む能力 (MCP) mizchiさんが作成したURLの先のサイトの本文抽出を行いmarkdownに変換するツールが紹介されていました。そのツールを用いて取得した内容をさらに要約してdocsに保存するとありました。cursorやgeminiではweb検索の機能はありますがそれを行うとコンテキストが膨れ上がり精度の低下を感じていたので検索、要約を明確に分けて要約した結果のみをプロンプトとして与えるのは有効な方法だなと思いました。 今回ピックアップしたもの以外にもそれぞれのプロンプトがうまくいった理由といかなかった理由を丁寧に説明されていました。また、それぞれのプロンプトがスライドに記述されていてすぐに使えるかたちになっていたのでTSKaigiが終了次第すぐ試してみようと思います! まとめ TSKaigi 2025は、TypeScriptを中心にしたカンファレンスとして、多くのエンジニアにとって有益な情報が得られるイベントでした。 TypeScriptの最新情報や活用事例を学ぶことができ、新しい技術やアイデアに触れることができました。 今後も、TypeScriptコミュニティの発展と、エンジニアのスキルアップに貢献するイベントとして、TSKaigiが続けられていくことを期待しています。 また、今回の参加レポートが、TypeScriptを学びたい方や、TypeScriptを活用したい方の参考になれば幸いです。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに こんにちは、デリッシュキッチン開発部でソフトウェアエンジニアをしている新谷です。 新卒で入社してから早1年が経ち、時の流れの速さを感じています。 今回は、アプリ課金システムにおけるサーバー側のテスト方法についてご紹介します。 最近、デリッシュキッチンとヘルシカにおけるアプリ課金システムのサーバー側の修正を行いました。 その際、テスト方法に苦戦したので、その内容をまとめたいと思います。 アプリ課金システムの概要 デリッシュキッチンとヘルシカでは、iOSとAndroidの両方でアプリ課金ができますが、今回はiOSの課金についてのご紹介です。 そもそもアプリ課金には、以下の2種類があります。 一度切りの買い切り型 サブスクリプション型(定期課金) 今回、扱うのはサブスクリプション型(定期課金)の課金です。 サブスクリプションの課金は、処理として大きく2つに分けられ、最初の購入処理とその後の通知処理です。 通知処理とは、Appleから通知される購読状態を受けて、ユーザーの状態を更新する処理です。 1つ目の購入処理の概要は以下の図のようになり、購入したレシートの検証とレシートの保存が主な処理です。 ここでは、subscription処理と呼ぶことにします。 2つ目の通知処理の概要は以下の図のようになり、ユーザーの課金状態を更新するのが主な処理です。 ここでは、notification処理と呼ぶことにします。 デリッシュキッチンとヘルシカでは、課金サーバーが分かれているので、厳密には上記の図ではないのですが、行っている処理は大枠では同じです。 ヘルシカの課金サーバーについては、以下の記事で解説されています。 tech.every.tv サーバー側でテストするべき箇所 サーバー側でテストするべき箇所は、subscription処理とnotification処理の内、課金サーバー側で行う処理です。 テスト項目については、今回省略しますが、基本的にはsubscription処理とnotification処理で、DBに保存しているレシートの情報が正しいかどうかをテストすることになります。 テスト方法 iOS課金には、テストする方法として、以下の2つがあり、どちらとも実際に課金しても請求は発生しません。 TestFlightでの課金 Sandboxアカウントでの課金 TestFlightでの課金 TestFlightでの課金は、TestFlightで配布されたテストバージョンのアプリで課金を行う方法です。 TestFlightで課金すると、更新頻度は通常より短くなり以下の表のようになります。 1週間 1ヶ月 2ヶ月 3ヶ月 6ヶ月 1年 更新頻度 1日 1日 1日 1日 1日 1日 詳しい情報は公式サイトを参照してください。 developer.apple.com Sandboxアカウントでの課金 Sandboxアカウントでの課金は、SandboxにしたいAppleアカウントをApp Store Connectに登録することで、Sandboxアカウントで課金を行えます。 Sandbox課金は、更新頻度はApp Store Connectで設定することができ、デフォルトが5分となっています。 表にすると以下のようになります。 更新頻度 1週間 1ヶ月 2ヶ月 3ヶ月 6ヶ月 1年 更新頻度 3分 5分 10分 15分 30分 1時間 更新頻度は以下の候補があります。 3分 5分(デフォルト) 30分 1時間 こちらも、詳しい情報は公式サイトを参照してください。 developer.apple.com TestFlightで配布しているアプリでSandbox課金を行う方法 TestFlightで課金する場合の注意点として、課金してしまうとキャンセルすることができません。また、更新頻度が1日に1回で1週間続くので、notification処理のテストが1日に1回しか確認できず、キャンセルまで1週間待つ必要があります。 そのため、基本テストするときはSandboxアカウントで課金を行いたいです。 しかし、TestFlightで配布しているアプリでは、TestFlightで登録されているAppleアカウントで課金させられてしまいます。 これは、TestFlightで配布しているアプリでは、TestFlightのアカウントで課金させられてしまうためです。 そのため、一度、Apple Storeからサインアウトを行うことで、TestFlightのアカウントからサインアウトします。 その後、課金をしようとすると以下のようにログイン画面に遷移します。 ここで、Sandboxアカウントでログインを行うことで、Sandbox課金を行うことができます。 まとめ 今回は、iOS課金のサーバー側のテスト方法についてご紹介しました。 Androidの課金については、またやり方が異なるので、また記事にしたいと思います。 iOS課金の実装やテストを行う際の参考になれば幸いです。
アバター
概要 TIMELINE開発部の内原です。 今回はAWS ALBに対するリクエスト時、送信先となるIPアドレスを固定する方法について調査しましたのでその共有です。そこまで一般的な要件ではない気はしますが、参考になれば幸いです。 背景 とある環境において、ALBに対する送信元側がIPアドレスのホワイトリスト形式で通信を許可する構成になっているため前述の要件を満たす必要がありました。 ただ、AWSのALBはIPアドレスが固定されておらず、状況によって変動するという仕様になっています。このため、DNSでALBを指定するにはALB DNS名をCNAMEで指定するか、Route53のAlias機能を用いて指定するのが一般的です。 このやり方だとIPアドレスが変動するため、接続元から接続先IPアドレスを固定したいというニーズは満たせません。 ALBの場合はEIPを用いて固定化することもできない仕様です。 解決策 いくつか選択肢が考えられます。 Global Accelerator(GA) を用いる GAは本来高速化、安定化の文脈で用いられる機能ですが、副次的に固定IPアドレス(2個)が付与され、このIPアドレス経由で通信可能になるためIPアドレスの固定化をすることができます。 GAはエンドポイントとしてALB, NLB, EC2, EIPを選択することが可能です。つまりALBに対するProxyとして機能し、その中でNetwork経路の最適化やMulti-AZでのfail overといった高速化、安定化が実現できます。 構築手順は以下の通りです。 Global Acceleratorを追加 リスナーを指定。今回はSSLのみ対応すればよいため443, TCPを指定 エンドポイントタイプとしてALB、ロードバランサとして既存ALBを選択 作成後、プロビジョニング完了を待つ 上記が完了するとGAのDNS名と固定IPアドレス(2個)が発行されます。またDNS名を解決すると2個のAレコードが返却されます。この際、片方のIPアドレスのみでも通信は可能ですが、可用性を考慮すると冗長な構成が望ましいためDNS経由での通信が適当かと考えます。 その際、SSL証明書はALBが保持しているものが利用されるため、接続する際のホスト名はSSL証明書と合致している必要があります。 Network Load Balancer(NLB) + Elastic IP(EIP) を用いる NLBは固定IPアドレスを付与することが可能なロードバランサーです。ALBと異なり、レイヤー4のロードバランサーとなるため、SSL終端はできません。そのため、SSL終端はALBで行い、NLBはALBの前段に配置する構成となります。 構築手順は以下の通りです。 NLBを作成 リスナーを指定。今回はSSLのみ対応すればよいため443, TCPを指定 ターゲットグループを作成し、既存ALBを指定 EIPを取得し、作成済みNLBにアタッチ 上記が完了するとNLBに固定IPアドレスが付与されます。またNLBのDNS名を解決するとEIPのAレコードが返却されるため、このIPアドレス経由で通信が可能になります。 その際、SSL証明書はALBが保持しているものが利用されるため、接続する際のホスト名はSSL証明書と合致している必要があります。 EC2 + Elastic IP(EIP) を用いる EC2インスタンスにReverse Proxy(例: Nginx)を構築し、EIPを付与する方法です。ALBの前段に配置し、ALBに対するProxyとして機能します。 構築手順は以下の通りです。 EC2インスタンスを作成 Nginx等のReverse Proxyをインストール Reverse Proxyの設定を行う EIPを取得し、EC2インスタンスにアタッチ 上記が完了するとEC2インスタンスに固定IPアドレスが付与されます。またDNSを解決するとEIPのAレコードが返却されるため、このIPアドレス経由で通信が可能になります。 その際、SSL証明書はEC2インスタンス上に設置する必要がありますが、ALBの証明書と異なっていてもよいことになります。 比較 それぞれの手法において特徴を確認します。 なおいずれの手法においても既存のALBの前段にアタッチする形式となるため、サービスのダウンタイムは発生しません。(ただし接続先の切り替え作業は必要です) また機能要件としては以下を想定します。 接続はSSL 東京リージョン(送信元は日本国内) 100GB/月のデータ転送 24時間稼働 比較表 今回のユースケースにおけるコストや機能についての比較表は以下です。 なお、GAやNLBにおいて最適な利用シーンは元々差異がありますが、今回の用途において特に影響がなかった部分は無視しています。 手法 費用 冗長性 経路最適化 SSL終端化 セキュリティ保護 GA GA本体 $0.025 × 24h × 30日 = $18.00 データ転送 $0.010 × 100GB = $1.00 IPアドレス 2個 × $0.005 × 24h × 30日 = $7.20 合計約 $26.20/月 ○ あり 不要 あり NLB+EIP NLB本体 $0.0243 × 24h × 30日 = $17.49 LCU $0.006 × 24h × 30日 = $4.32 EIP $0.005 × 24h × 30日 = $3.60 合計約 $25.41/月 ○ なし 不要 なし EC2+EIP EC2 (t3.micro) $0.0136 × 24h × 30日 = $9.79 データ転送 $0.114 × 100GB = $11.40 EIP $0.005 × 24h × 30日 = $3.60 合計 約 $24.79/月 △ なし 必要 なし(要実装) コスト的にはどの選択肢も大きな違いはないようです。 ただEC2+EIPは別途SSL終端化が必要であり、かつ別途インスタンスのメンテナンスも必要となり運用コストが高くなります。また冗長性にも課題があるため、この選択肢を採る利点は薄いように思われます。 GAとNLB+EIPとではコスト的には多少後者が低いもののほぼ同レベルです。GAのほうは経路最適化が可能ですが、日本国内間の通信であればさほど影響はないかもしれません。 GAのほうが新機能であることを踏まえると、より活発な機能追加が行われる可能性は高そうなのでやや有利と言えるかもしれません。 まとめ ALBのIPアドレスを固定化する複数の手法について比較検討してみました。 今回のユースケースにおいてはGAを用いるのが一番適当に思われますが、状況によっては別の選択肢もあり得そうです。
アバター
目次 はじめに JSAI とは? エブリーにおける AI 利用に関する取り組み イベント当日について 最後に はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 この度、株式会社エブリーは、2025 年 5 月 27 日(火)から 30 日(金)に開催される「JSAI2025 (2025 年度 人工知能学会全国大会)」に、プラチナスポンサーとして協賛することになりました! www.ai-gakkai.or.jp (2025/06/04追記) 参加レポートはこちら tech.every.tv JSAI とは? JSAI は、日本人工知能学会の略称です。人工知能に関する研究の進展と知識の普及を図り、学術・技術ならびに産業・社会の発展に寄与することを目的として設立された、日本の学会です。 今年で 39 回目を迎える全国大会は、大阪の大阪国際会議場(グランキューブ大阪)で開催されます。 エブリーにおける AI 利用に関する取り組み メインサービスである「デリッシュキッチン」では、「作りたい!が見つかる」をサービスのコンセプトとして、様々な機能を提供してきました。 一方、ユーザーひとりひとりの多様なニーズに合わせたレシピを提案していくには既存機能だけでは難しい部分があり、AI による料理アシスタントとして「デリッシュ AI」を一部ユーザー向けに提供し始めています。 tech.every.tv また、これらの AI 活用を支える CI/CD パイプラインの構成にも力を入れています。 その中で使っている Databricks Asset Bundles は、データや AI プロジェクトにおいて、ソフトウェア開発におけるソース管理、コードレビュー、テスト、CI/CD といったプラクティスの導入を容易にするツールです。これにより、Databricks の各種リソースを Infrastructure-as-Code(IaC)として管理しています。 tech.every.tv 「デリッシュ AI」の内部では OpenAI API を利用しており Structured Outputs についても触れているので、ぜひご覧ください。 tech.every.tv イベント当日について 当日は弊社ブースにて、「デリッシュ AI」のポスター展示やデモを行います。 また、ノベルティも配布予定ですので、ぜひご興味のある方はお越しください! 最後に エブリーでは、ともに働く仲間を募集しています。 このテックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。 今回は、挑戦WEEKで実装した「レシピ材料の同義語辞書自動化」をLLMで実装した内容をまとめます。 挑戦WEEKに関しては、以下の記事をご覧ください。 tech.every.tv 背景 ユーザーのクエリによって、同じ意味を表す言葉でも異なる単語が使われることがあります。 デリッシュキッチンを題材に例を挙げると「鶏もも肉」「とりもも肉」「鳥もも肉」などです。 これらの単語同士を同義語(シノニム)、これらの同義語を対応づけたものを同義語辞書と呼びます。 デリッシュキッチンの検索機能では同義語辞書を人手で作成して対応しています。 これは先人たちの苦労が垣間見える瞬間でもあるのですが、検索機能以外で同義語辞書が必要になる場面が多数出てきており、検索機能と同様の運用を継続することは難しくなってきました。 そこで、以下の記事を参考に、同義語辞書の自動化を挑戦WEEKの題材として実装してみました。 約3年前の記事ですが、OpenAI APIなど容易にLLMが活用できる今日では、より簡易的に実装できるようになっていると思います。 engineering.mercari.com 手法 記事を参考に、以下のような手順で同義語辞書を作成しました。 検索結果に表示されているレシピをタップした時に得られる、その検索キーワードとレシピ材料のペアデータを抽出する。 検索キーワードを単語に分割する。 検索キーワード中の単語とレシピの材料の出現回数と共起回数を計算する。 それぞれの語の出現回数と共起回数をもとにNPMI(詳細は後述)を計算する。 NPMIをもとに同義語ペアの候補を並びかえ、上位5万ペアを抽出する。 OpenAIのEmbedding APIを用いて同義語ペア(検索キーワードと材料名)をそれぞれベクトルに変換する。 コサイン類似度を計算し、コサイン類似度とNPMIをもとにそれぞれの閾値を設定して、同義語辞書を出力する。 1. 検索キーワードとレシピ材料のペアデータを抽出 まず、検索ログと視聴ログをJOINして、検索キーワードとそれに紐づくユーザーID, レシピIDを取得します。 検索ログには検索キーワードとユーザーID、視聴ログにはユーザーIDとレシピIDが記録されており、ユーザーIDと日付が一致するものをJOINしました。 厳密に検索→視聴の順にログが記録されたことを保証するには、unixtimeが検索<視聴になっている必要があります。 しかし、ここではユーザーが検索を利用する際は特定の目的を持っていると仮定し、検索→視聴、視聴→検索のどちらの順序でも、同一の興味を持ったユーザー行動として扱うことにしました。 つまり、同じ日のユーザー行動であれば、検索、視聴のどちらが先だったとしても、等しく共起したペアとみなし、そのペアを同義語ペアの候補として抽出しています。 あくまでユーザーの検索キーワードとレシピの材料の共起に関心があり、時系列を深く気にしない方針を取りました。 2. 検索キーワードの分割 以下の文字で、検索キーワードを分割しました。 半角スペース 全角スペース 読点 カンマ 厳密に単語だけを抽出する場合は形態素解析するなどの前処理が必要ですが、デリッシュキッチンの検索キーワードの多くは単語区切りであることが多いため、これだけでも検証は進められました。 また、レシピの材料に関しては、マスターデータとして1レシピに対して複数の材料が紐づけられています。 材料データは原則単語でまとまっているため、ほとんど前処理することなくそのまま使用しました。 最終的に検索キーワードとレシピ材料のペアデータが以下のようになります。 例として、あるユーザーAとBによって、検索キーワードが「鳥もも肉 照り焼き」「とりもも肉 照り焼き」という検索がされ、そのレシピ結果で表示されたレシピが視聴されたとします。 検索キーワード ユーザーID レシピID レシピ材料 鳥もも肉 A 9876543210 鶏もも肉 鳥もも肉 A 9876543210 しょうゆ 鳥もも肉 A 9876543210 みりん 照り焼き A 9876543210 鶏もも肉 照り焼き A 9876543210 しょうゆ 照り焼き A 9876543210 みりん とりもも肉 B 9876543210 鶏もも肉 とりもも肉 B 9876543210 しょうゆ とりもも肉 B 9876543210 みりん 照り焼き B 9876543210 鶏もも肉 照り焼き B 9876543210 しょうゆ 照り焼き B 9876543210 みりん 3. 検索キーワード中の単語とレシピの材料の出現回数と共起回数を計算 検証のため、2025年3月23日の1日のみのデータを使用しました。 1日の検索ログと視聴ログをペアデータとして、それぞれの出現回数と共起回数を計算します。 共起回数だけだと、どのレシピにも登場する調味料などが多く出現していまいます。 後続のNPMIの計算では、この性質を抑制するのに働きます。 なお、PMIが共起回数が少ないと比較的高い値になりやすいため、共起回数が100回以上のペアデータのみを抽出しました。 データのスケールは異なりますが、参考記事の工夫点を踏襲しています。 4. NPMIの値を計算 検索キーワードの出現確率をP(x)、レシピ材料の出現確率をP(y)、共起確率をP(x, y)とし、以下の式に従って、NPMIを計算します。 しかし、理論上では確率ですが、実務では出現回数と共起回数をもとに計算することが多いかと思います。 検索キーワードの出現回数をC(x)、レシピ材料の出現回数をC(y)、共起回数をC(x, y)、ペアデータの総数をNとし、以下の式に従って、NPMIを計算します。 PMIからNPMIを計算することで、値が-1~1の間に収まります。 これは直感的で解釈しやすくなることがわかると思います。 PMIでは集計するたびに変動があった場合、閾値を設定することが難しくなりますが、NPMIでは-1~1の間に収まるため、閾値を設定することが容易になります。 また、データのスケールが異なる場合(検証は1日のデータですが、実運用は1ヶ月分のデータにしたいなど)にも対応できます。 5. 同義語ペアの候補を抽出 NPMIの値をもとに、上位5万件を同義語ペアの候補として抽出しました。 この数値は、OpenAIのBatch APIの上限に合わせました。 platform.openai.com 6. 同義語ペアのベクトル化 強い共起があったとしても、その検索キーワードとレシピの材料が似ているとは限りません。 例えば、検索キーワードが「ホットケーキミックス」、レシピの材料が「無塩バター」など、お菓子作りの王道パターンのような組み合わせもNPMIは高くなります。 このような場合では、単語同士がどれだけ似ているかも考慮したくなります。 そこで、OpenAIのEmbedding APIを用いて、検索キーワードとレシピの材料それぞれに対してベクトルを取得します。 7. 同義語辞書の出力 それぞれベクトルをもとに、コサイン類似度を計算します。 これにより、NPMIとコサイン類似度の結果をもとに、それぞれの閾値を決めることで同義語ペアを抽出できます。 ここで抽出した同義語ペアを最終的に同義語辞書として記録します。 以降、この処理を動かし続ければ、日々のユーザー行動を元に同義語辞書を更新できるといった流れです。 結果 NPMIとコサイン類似度を散布図として可視化しました。 検索キーワードとレシピの材料が完全一致するものを1、それ以外を0としてラベル付けしました。 以降、ラベルが0のデータを結果の対象にします。 NPMIを0.2に固定し、コサイン類似度を変化させた時の結果を以下に示します。 閾値は赤い点線として表現しました。 検索キーワードと材料は閾値付近ものを20件ずつ抽出しています。 NPMI>=0.2 コサイン類似度>=0.6 NPMI>=0.2 コサイン類似度>=0.8 NPMI>=0.2 コサイン類似度上位20件 散布図 同義語ペア 結果 「きなこ」と「きな粉」は問題なさそうですが、「トマトパスタ」と「トマトソース」のようにレシピ名の検索キーワードと材料が同義語ペアにになりました。 閾値が高いほどマシになりました。しかし、「小松菜炒め」と「小松菜」のようにレシピ名と材料が同義語ペアになる問題はここでも解決しません。 「わさびな」と「わさび」など、食材としては別物でも名前は似ていることが原因で同義語ペアに含まれるケースがあることがわかりました。 まとめ 同義語辞書を自動化する仕組みを検証しました。 1日のデータのみを使用した検証ですが、OpenAI APIをフル活用することで、短期間で定性的な検証ができたと思います。 検索機能のロジックに依存する問題(極端に言えば、検索ロジックがランダムに返す場合に破綻するなど)はありますが、ユーザーが入力する検索キーワードをもとに、ある程度の同義語辞書であれば自動化可能なことが確認できたのは良かったです。 今後の課題としては、検索キーワードがレシピ名のときに、材料と同義語ペアになる問題の解消があります。 今回は検索キーワードと材料の共起に着目しましたが、レシピ名もマスターデータとして管理されているため、検索キーワードとレシピ名の共起も考慮する必要があると考えています。例えば、レシピ名との共起性が強い検索キーワードは、材料との同義語ペアにしないなどの工夫ができるかもしれません。 検証全体を通して、完璧な同義語辞書を自動化するまでは難しいなという所感です。 「わさびな」と「わさび」のケースが今回見つかりましたが、似たようなケースが他にも存在するかもしれません。 ただし、完璧な同義語辞書でなくても問題なく運用できる場合もあると思いますので、そのようなケースにおいてこの手法をブラッシュアップし、プロダクトに導入できればと考えています。
アバター