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をフル活用することで、短期間で定性的な検証ができたと思います。 検索機能のロジックに依存する問題(極端に言えば、検索ロジックがランダムに返す場合に破綻するなど)はありますが、ユーザーが入力する検索キーワードをもとに、ある程度の同義語辞書であれば自動化可能なことが確認できたのは良かったです。 今後の課題としては、検索キーワードがレシピ名のときに、材料と同義語ペアになる問題の解消があります。 今回は検索キーワードと材料の共起に着目しましたが、レシピ名もマスターデータとして管理されているため、検索キーワードとレシピ名の共起も考慮する必要があると考えています。例えば、レシピ名との共起性が強い検索キーワードは、材料との同義語ペアにしないなどの工夫ができるかもしれません。 検証全体を通して、完璧な同義語辞書を自動化するまでは難しいなという所感です。 「わさびな」と「わさび」のケースが今回見つかりましたが、似たようなケースが他にも存在するかもしれません。 ただし、完璧な同義語辞書でなくても問題なく運用できる場合もあると思いますので、そのようなケースにおいてこの手法をブラッシュアップし、プロダクトに導入できればと考えています。
目次 はじめに 2024年度の振り返りと改善点 成果 課題 課題の解消に向けた施策について 研修の目的・カリキュラム 研修の目的 カリキュラム CTO からのメッセージ 活躍した新卒2年目エンジニアからのメッセージ バックエンド/インフラ講義 モバイル講義 Web 講義 データ講義 次回に向けて おわりに はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 本記事では、2024年度から実施を始め、第2回目となる2025年新卒エンジニア向け研修の取り組みについてご紹介します。 昨年度実施した新卒研修については、以下をご覧ください。 2024年度の振り返りと改善点 昨年度のエンジニア新卒研修では、以下のような成果と課題が見られました。 成果 エブリーのエンジニアとしてのマインドセットの理解 各技術領域の全体像の把握 社内エンジニアとのネットワーク構築 課題 前提知識の不足による理解の困難さ 各種ツールの使い方に関する事前準備の不足 課題の解消に向けた施策について 昨年度の課題を踏まえ、運営チームでは如何に入社時の前提知識の不足やばらつきを解消するのかを検討していました。 その中で、入社前から新卒(当時は内定者)の意欲が高く、学習コンテンツがあれば積極的に活用したいという声が多くありました。 そこで、内定者研修という形で入社後のオンボーディングをスムーズにするための基礎的なキャッチアップをフォローする研修を実施しました。 内定者研修についてのブログも公開しておりますので、ぜひご覧ください。 研修の目的・カリキュラム 今年度の研修では、昨年度のフォーマットを踏襲しつつ、成果や課題を踏まえて、改善策なども検討しました。 研修の目的 研修自体の目的や方針については、昨年度と大きくは変えず、以下のように設定しました。 目的 『オーナシップを持ってプロダクト課題の解決に動けるエンジニアへの土台を作る』 方針 エブリーのエンジニアとして求められるマインドを理解する 専門領域を超えてエブリー全体で使われている技術スタックを理解する カリキュラム 研修のカリキュラムについても、昨年度と大枠は変わっていません。 新卒の皆さんには、5日間で以下のようなカリキュラムに従って研修に参加してもらいました。 マインド研修 CTO からのメッセージ 活躍した新卒2年目エンジニアからのメッセージ 技術スタックの把握(講義 + ハンズオン) バックエンド/インフラ モバイル Web データ ランチ会 前回からの変更点として、マインド面の研修に盛り込んでいたインシデントへの向き合い方のパートが変更になっています。 講義内容をより身近に感じてもらうために、活躍している新卒2年目エンジニアからのメッセージとして同様の要素を取り込みつつ、実際に入社直後から活躍している先輩がどのようなことを意識しながら業務に取り組んできたのかを伝えてもらいました。 CTO からのメッセージ この講義では、AI/LLM時代におけるエブリーのエンジニアとして重要となるマインドセットについて講義をしました。 生成AIが破竹の勢いで進歩していく中でエンジニアを取り巻く環境はどう変わっていくのか、また、エブリーのエンジニアとしてどういう姿勢で臨むべきかについて、CTO 自らの視点を交えた内容となり、配属に向けて刺激を得られる講義でした。 活躍した新卒2年目エンジニアからのメッセージ この講義では、入社直後から活躍している先輩がどのようなことを意識しながら業務に取り組んできたのかを伝えてもらいました。 実際に活躍している近い世代の先輩のマインドを伝えることで、実際に行動に移した例もあり、より具体的なイメージを持ってもらうことができたと思います。 バックエンド/インフラ講義 バックエンド/インフラ講義では、エブリーで共通的に用いられる技術や知識について、一通り触れて理解することを主な目標としました。 ハンズオンを通して以下の内容を学びました。 社内の技術スタック Go を利用した簡易的な API サーバの開発体験 CLIによる手動デプロイからCDによる自動デプロイまでを体験 モバイル講義 モバイル講義では、アプリの開発の基礎や実装に触れてもらい、API 設計やログ解析、不具合解析の際にアプリサイドの開発観点も考慮できるようになることを目標としました。 主な内容: モバイルアプリ開発の基本概念 基本的な UI 実装 アプリログや分析の実装 API接続ハンズオン 組織開発における注意点 Web 講義 Web 講義では、Webの歴史から現在の技術を理解することやWeb開発の基礎知識を身につけることを主な目標としました。 講義内容: 社内の技術スタック Web 開発の歴史と基礎知識 開発における重要なポイント デリッシュキッチンアプリを題材として画面実装ハンズオン データ講義 データ講義では、データ領域の各分野について体系的に学びました。 主な内容: 各種職能の役割 社内での業務への関わり方 AIの概観/社内での活用事例 データ基盤 データ品質の管理 実践的なデータ処理ハンズオン 次回に向けて 研修後の調査で、受講者からは以下のように好意的なフィードバックが寄せられました。 同期と楽しみながら進められた 研修を通して学ぶべきことを知ることができた 全領域を横断的に触れることで業務でのコミュニケーションにも繋げられると感じた 研修を楽しみながら、エブリーのエンジニアとして必要となるものを認識してもらうことができ、目的は達成できたのではないかと思います。 しかし、改善点がなくなったわけではありません。 特に、生成AIの進歩に対応したコンテンツ作りは1つの大きな懸念材料になると思います。 弊社では、AIエディタである Cursor がエンジニア(およびPdM)を対象に導入されました 1 。 組織全体としても模索中である開発業務におけるAI活用はもちろん、受講者のAI利用を前提としたコンテンツをどのように構成するべきかを検討する必要があると考えています。 講義風景 講義風景 おわりに 2025年度のエンジニア新卒研修では、2024年度の課題を踏まえた改善を実施し、より充実した研修プログラムを提供することができました。 配属前のタイミングで横断的なコンテンツを提供することで、エンジニアとしてだけではなく業務に携わるメンバーとしても、早期成長の手助けをできていると感じています。 今後も、研修などを通じてエブリー開発組織全体の活性化・強化を目指していきたいと思います。 最後に、本研修の実施にあたり、講義に快くご協力いただいた皆様に感謝申し上げます。 エブリー、AIエディタ「Cursor」を全エンジニアおよびプロダクトマネージャーに導入 ↩
はじめに こんにちは、デリッシュキッチンでクライアントエンジニアを担当している kikuchi です。 デリッシュキッチンの Android アプリ開発チームでは新規機能の開発だけでなく、日々アプリの改善のために不具合の修正や挙動の改善についても力を入れており、アプリのパフォーマンスの部分など細かい部分も数値が改善されているかシビアに計測データで確認しています。 今回はそのパフォーマンスの部分で 「アプリの起動時間の改善」 の観点で特に効果が高かった対応をまとめてみたいと思います。 アプリの起動時間を確認する方法 アプリ起動時間は以下で調べることが出来ます。 Google Play Console の Android Vitals でコールドスタートなどの時間を確認する アプリの TTID (Time To Initial Display) と TTFD (Time To Full Display) を確認する ログを仕込む 1 については、Google Play Console 上で Android Vitals という機能を使ってコールドスタートなどの数値を確認する、というものになります。 こちらは OS のバージョン、CPU、RAM などで細かくフィルタリングができ、また何より 類似するサービスのアプリや、任意で選択したアプリと比較できる という点が他社のアプリと直接比較できる数少ない指標になるため重宝しています。 (なお、実値で比較できるわけではなく、あくまでコールドスタートなどの時間が長いユーザの割合が比較できるのみ) 2 については Google Developers の公式サイトで詳しく紹介されています。 TTID : 初期表示までの時間のことで、アプリの UI の最初のフレームを表示するまでにかかる時間 https://developer.android.com/topic/performance/vitals/launch-time?hl=ja#time-initial TTFD : 完全表示までの時間のことで、アプリがユーザ操作可能な状態になるまでの時間 https://developer.android.com/topic/performance/vitals/launch-time?hl=ja#time-full TTID についてはどのアプリについても Logcat で簡単に確認することが出来ます。 以下は API Level 36 のエミュレータで Chrome アプリを起動した場合のログになります。 Displayed でフィルタをかけて表示したログで、「1s312ms」という数値が TTID となります。 上記のようにアプリを起動するだけで出力されるログのため、自社で開発したアプリ、ストアからインストールしたアプリなど関係なく確認することが出来ます。 なお、TTFD については ComponentActivity の reportFullyDrawn() というメソッドを意図的に呼ぶ必要があるため、基本は自社で開発したアプリのみで確認できます。 TTFD については今回まとめる内容と深く関わる部分ではないため、割愛させていただきます。 3 については、細かく Log クラスなどを駆使してログを仕込んでいく方法になります。 こちらはあくまでアプリが起動してから (つまりは Application を継承したクラスの onCreate メソッドがコールされてから) の処理にログを仕込めるものとなるため、怪しい実装が無いかを特定する際に最適な方法だと言えます。 調査観点 起動時間を改善する、と一言で言っても、手当たり次第調査をしても効果は出にくいと考えたため、以下に観点を絞って調査をしました。 アプリ起動時に不要な処理を実行していないか 不要なライブラリが存在しないか 起動時間そのものを最適化して短縮できないか 結果を先にお伝えすると、1 についてはログを細かく仕込んで調査したものの全く問題なく、余計な処理や大きく短縮できそうな処理は存在せず、2 と 3 に大きく効果がある改善ポイントが存在していました。 以降の項目で 2 と 3 について細かくまとめていきたいと思います。 不要ライブラリの調査 不要ライブラリが存在するかについては、導入しているライブラリを一つ一つ確認していきました。 とはいえ、デリッシュキッチンのプログラムの規模はかなり大きく全部で 100 個近くのライブラリを導入しているため、AndroidX など必須のライブラリは除いて画像関連のライブラリなどサードパーティ製のライブラリに絞って調査することにしました。 結果、 Firebase In-App Messaging がかなり起動時間に悪影響を与えていたということが分かりました。 複数端末で Firebase In-App Messaging を除外した状態で TTID を検証した結果は以下のようになりました。 デバイス A / OS 8.1.0 / RAM 4GB … 180msec 程度改善 デバイス B / OS 12 / RAM 6GB … 120msec 程度改善 デバイス C / OS 13 / RAM 8GB … 30msec 程度改善 OS が古い、RAM が少ない端末ほど改善幅が大きい、という結果になりました。 こちらについてはデリッシュキッチンチーム全体で確認した結果、今は使用しておらず今後も使う可能性は無い、ということが分かったためライブラリを除外する結論となりました。 何故 Firebase In-App Messaging が起動時間に悪影響を与えていたのか In-App Messaging ですが、どこでもメッセージを表示できるという特性上、 WebView で作られていたことが起動時間を長くしていた要因でした。 WebView の初期化、生成は非常にコストがかかるうえ、Firebase In-App Messaging は ライブラリを導入しているだけで問答無用でアプリ起動時に初期化される 仕組みとなっており、意図的にプログラムで初期化処理を呼ばなくても初期化処理が走っていました。 結果、アプリ起動時に WebView の初期化処理まで走って余計に時間がかかっているという状況に繋がっていました。 他にも In-App Messaging の機能を提供しているライブラリでも同様に初期化処理に時間がかかっていたため、In-App Messaging のライブラリを使用している場合はライブラリの見直しをしてみるのも良いかも知れません。 起動時間そのものを最適化する方法の調査 起動時間そのものを最適化する方法については、Android Gradle Plugin (AGP) と Android Jetpack の一部として提供されている Baseline Profiles という仕組みが導入コストが低く、改善の期待値が高いということが分かりました。 Baseline Profiles については Google Developers に概要がまとめられています。 https://developer.android.com/topic/performance/baselineprofiles/overview?hl=ja Baseline Profiles とは、アプリの起動に必要な情報を事前 (AOT) コンパイルしておくことで、アプリの起動時に実行するコードパスの解釈、ジャストインタイム (JIT) コンパイルを省略でき、初回起動からのコード実行速度が約 30% も向上するものとなります。 なお、Baseline Profiles を採用することで起動時間を短縮するだけでなく、ジャンク (かくつき) の軽減、全体的なパフォーマンスの向上にも繋がります。 Baseline Profiles の導入自体は容易です。次の項目で導入手順をまとめます。 Baseline Profiles の導入手順 実際にプロジェクトに追加する手順を画像を交えて紹介します。 ①Android Studio で「File > New > New Module」を選択 ②Templates で「Baseline Profile Generator」を選択し、必要に応じて値を設定し、Finish を選択 ※「Use Gradle Managed Device」にチェックを付けることで、Gradle で管理されているデバイス (GMD) を使用して Baseline Profiles を作成するための初期設定が自動的に行われます ③ツールバーの Run / Debug Configrations で、手順 2 にて生成した項目を選択して Run を実行 ④app モジュール > src > release / generated / baselineProfiles に baseline-prof.txt が出力される 以上で完了となります。 ※実際のプロジェクトの環境によっては手順が異なる可能性があるため、先述した公式サイトの情報を確認しながら設定を行うことをおすすめします。 Baseline Profiles の導入結果 Baseline Profiles を導入した結果の TTID は以下のようになりました。 デバイス A / OS 8.1.0 / RAM 4GB … 200msec 程度改善 デバイス B / OS 12 / RAM 6GB … 150msec 程度改善 デバイス C / OS 13 / RAM 8GB … 70msec 程度改善 こちらも不要ライブラリ除外と同様、OS が古い、RAM が少ない端末ほど改善幅が大きい、という結果になりました。 Baseline Profiles についてはプロジェクトの規模などにも大きく左右されますが、大きな改善が見込めることが分かります。 対応した結果 今回注力して対応した「不要ライブラリ除外」、「Baseline Profiles の導入」のみで、 最大で 400msec 近くもの起動時間を短縮 に成功しました。 調査や対応に当然工数はかかっているものの、検証込みで 1 週間程度の少ない工数の見返りとしてはかなり大きいものだったと感じています。 これ以外にも TTID 以外の指標を使用して細かい計測やソースのリファクタリングなどを経て、最終的には対応前と対応後で起動時間を半分にすることが出来ました。 なお、起動時間を短縮した結果、一番恩恵を受けたのがスプラッシュ画面になりました。 OS 12 以降はスプラッシュ画面 (Splash Screen) が表示されるようになりましたが、TTID が短いほどスプラッシュ画面の表示が短くなります。 スプラッシュ画面はアイコンだけの画面になっていて無機質な画面に見えることが多く、スプラッシュ画面の表示時間が長いとフリーズしているように見えるため、すぐにユーザが操作できる状態になることはユーザビリティの向上に繋がります。 おわりに Firebase In-App Messaging の除外、Baseline Profiles 導入といった工数が少ない対応で大幅な起動時間の短縮に繋げることが出来ました。 デリッシュキッチンのようにライブラリの見直しだけでも起動時間の改善に繋がることもあるので、バージョン更新だけでなく、使用有無の棚卸しをするだけでも非常に効果的だと考えます。 また Baseline Profiles についても、こういったことをやりたい、と漠然と気になったことでも調べてみると機能が提供されていることがあるため、エンジニアの原点ではありますが、気になったら調べる、という行動は必要不可欠だと考えます。 本記事の情報が皆様のお役に立てれば幸いです。
はじめに こんにちは。リテールハブ開発部の池です。 エブリーは 2025/05/02 にプレスリリースを出した通り Cursor を全エンジニアとプロダクトマネージャーに導入し、AI活用による生産性の向上に積極的に取り組んでいます。 corp.every.tv 現在、世の中では Cursor のような開発支援ツールを使ってLLMをベースとしたエージェントの開発ワークフローを構築する動きが進んでいます。 エージェントに安定した挙動をさせるには、一貫したコンテキストの提供が必要であり、永続的なルールやファイルを用いてその振る舞いを制御するアプローチが一般的になりつつあります。 これには単なるドキュメント整備ではなく、ルールの構造化された設計が必要です。 どのような種類のファイルが必要か どのフォーマット・テンプレートで記述するのか エージェントはどのタイミングでそれを参照するのか どのように内容を更新して永続化し続けるのか こうしたルール設計は現状はまだ標準が存在せず、黎明期にあると感じています。 そのような環境において、私にとって有用な情報源となっているのが Reddit や Discord、X などの Cursor コミュニティです。 コミュニティでは多くのユーザーが日々の試行錯誤を共有しあっているため、ユーザーの実体験に基づいた新しい知見をキャッチアップするには非常に役立ちます。 この記事では、コンテキスト管理の必要性を説明した後、コミュニティの中で興味を持った Cursor Custom modes を活用したワークフローの手法 vanzen01/cursor-memory-bank について、試してみたいと思います。 ※ Cursor 公式のワークフローではないのでご注意ください。 LLMを活用した開発における課題 まず前提として、LLMは、コンテキストウィンドウを超えた情報を保持できず、基本的には直近のトークンのみ参照します。 つまり、一定のトークン数を超えると文脈が途切れ、過去のやりとりを忘れてしまうような現象が起こります。 (ただし、モデルによっては古い情報が影響を弱めながら残るケースもあるようです。) この問題は、プロジェクトの開発を進める中で頻繁に起こるので、文脈の持続が求められるタスクにおいて進捗を悪くする要因になります。 そのため、コンテキストを効率よく管理して適切な量の情報をLLMに渡すことが鍵となってきます。 コンテキストウィンドウとは コンテキストウィンドウとは、モデルが一度のプロンプトや会話履歴として「認識・参照」できるトークンの最大量のことです。 コンテキストサイズの比較表(2025年5月時点)は次のようになっています。このウィンドウサイズに収まるようにコンテキスト管理を行うことが重要です。 また、コード生成において、コンテキストサイズを超えずとも回答精度が悪くなるという声もあるため、コンテキスト量を抑えることが大事になります。 2025年5月時点における、主要なモデルのウィンドウサイズは以下のようになっています。 モデル名 コンテキストウィンドウサイズ リリース時期 GPT-4.1 1,000,000トークン 2025年4月 GPT-4.5 128,000トークン 2025年2月 o3 200,000トークン(入力)/100,000トークン(出力) 2025年4月 o4-mini 200,000トークン(入力)/100,000トークン(出力) 2025年4月 o4-mini-high 200,000トークン(入力)/100,000トークン(出力) 2025年4月 Claude 3.5 Sonnet 200,000トークン 2024年6月 Claude 3.7 Sonnet 200,000トークン 2025年2月 Gemini 2.5 Pro 1,000,000トークン 2025年4月 また、トークン数に加えて各モデルにおいて得意/不得意な作業があるため、それを考慮してモデル選択を行うことが大事です。 コンテキスト管理のアプローチ コンテキスト管理は様々なアプローチが考えられます。 1. タスクの分割と設計 トークン制限を考慮して、タスクを小さく分割し、それぞれのタスクに必要なコンテキストだけを提供することで、精度を保つことができます。 詳しくは本記事では触れませんが、タスク管理については claude-task-master を活用する事例をコミュニティで よく目にするので、よかったら参照してください。 2. メモリバンクの活用 メモリバンクとは、エージェントが参照すべき知識や振る舞いのルールを構造化して保存する仕組みです。 ここで扱っているメモリバンクはあくまで外部ドキュメントを明示的にコンテキストとして毎回渡す設計であり、モデル自体に永続的に記憶されるわけではありません。 詳しくは Cline Memory Bank を参照ください。 上記はClineが提唱するメモリバンクですが、Cline専用の機能ではなく方法論であるため、同じ仕組みをCursorに適用することができます。 3. モデルの使い分け モデルごとに特性やコンテキストウィンドウサイズが異なるため、タスクの性質に応じて最適なモデルを選択することで、コンテキスト効率と精度の両方を向上させることができます。 私は直近では以下の二つを使い分けています。 Claude 3.7 Sonnet:比較的小さいコンテキストで高精度が求められる場面、特にコード理解や分析タスク Gemini 2.5 Pro:大規模な設計・全体構成把握など、複雑なプロジェクト構造の理解 4. ワークフローの定義 ワークフローを明確に定義し、作業ごとのコンテキストを効率的に引き継ぐことが重要です。 例えば、「計画→実装→テスト→レビュー」という流れで、各ステップに必要な情報だけを選択的に提供することで、トークン消費を抑えながら一貫性を保てます。 5. 外部からコンテキストを取得する コンテキスト管理からは少し外れますが、MCPやRAG(検索拡張生成)などを利用して、社内の文書管理システムやFigmaの設計データなどを動的に検索・取得し、必要に応じてLLMに渡すことで、リポジトリ外の情報も活用できます。 リポジトリで管理できない社内ドキュメントや、テキストとしてコピーしづらいようなFigmaデザインデータなどをMCPやRAGなどを通じて取得・更新すると便利です。 Cursor Custom modes を利用した cursor-memory-bank を試す 以上のコンテキスト管理の必要性とアプローチ方法を踏まえつつ、ここからは2025年3月 Cursor v0.48 で搭載されたカスタムモードを利用したコンテキスト管理のワークフローについて、私が興味を持った下記プロジェクトの手法を試してみます。 github.com このプロジェクトではCursorのカスタムモードを活用し、AIがセッション間での記憶を維持しながら、構造化された開発ワークフローを実現することを目的としています。 本記事は Cursor v0.48 時点での設定方法に基づいています。以降のバージョンでUIや設定項目が変更される可能性があるのでご注意ください。 また、cursor-memory-bank も v0.6-beta 時点のワークフローに基づいており、ちょうど執筆時点で v0.7-beta にアップデートされていたため、最新のワークフローとは多少異なっています。 Cursor Custom modes とは Custom modes とは、Cursor のモードを独自に定義できる機能です。使用するツール、モデル、ルール等を設定した独自のモードを作成することができます。 このカスタムモードを活用することで、例えばコード生成時とコードレビュー時で異なるルールセットを適用できるため、それぞれのワークフローに特化した作業を行わせることができるようになります。 Custom modes 始め方 1. Cursor Settings > Features の Custom modes を許可する 2. Chatセクションのモード選択 > Add custom mode を選択 3. 必要なルールを設定する cursor-memory-bank を試してみる cursor-memory-bank は Cursor のカスタムモードを通じて、以下の4つの開発フェーズに対応するモードを提供します。 VANモード:プロジェクトの初期化とメモリバンクの設定 PLANモード:タスクの計画と戦略の立案 CREATIVEモード:設計とアイデアの創出 IMPLEMENTモード:実装と進捗の管理 これにより、各ワークフローに特化した一貫性のある開発を行います。 ※ v0.6-beta 時点のワークフローです。 VAN → PLAN → CREATIVE → IMPLEMENT という順に実行して開発を行います。 設定方法 cursor-memory-bank をプロジェクトに適用する手順は以下の通りです: 1. リポジトリのクローン cursor-memory-bankをクローンし、作業するプロジェクトに移します。 git clone https://github.com/vanzan01/cursor-memory-bank.git 2. Custom modes の設定 各開発フェーズのモード(VAN, PLAN, CREATIVE, IMPLEMENT)を作成します。 例)PLANモードの設定 エージェントへの指示もPLANモード用の内容をカスタムモードに設定します。 詳細な設定方法はcursor-memory-bankのREADMEに記載があるのでそちらを参照ください。 実行方法 次に、各モードを使った実際の実行例を紹介します。 サーバー側で、モバイルアプリの強制アップデートが必要かどうかを判定するミドルウェア機能を実装するタスクが、ちょうど良い大きさのタスクだったので、その機能の実装で試します。 VANモード カスタムモードで設定したVANモードを選択して「VAN」と入力すると、プロジェクト全体を読み込み、メモリバンクの初期化を行います。 ただし、今回は使い方や設定に誤りがあったのか、この方法ではメモリバンクの各ファイルの内容が生成されませんでした。 そこで、今回は gemini-2.5-pro-exp-03-25 モデルを利用し、チャット上で指示をしてプロジェクト全体のコンテキストをドキュメント化しました。 Gemini 2.5 Pro はコンテキストウィンドウサイズが1Mと大きく、プロジェクト全体を解釈して設計や計画立てを行うことが得意であるため、 メモリバンクのコンテキスト生成時に利用すると比較的詳細にドキュメント化してくれます。 PLANモード PLANモードは memory-bank/tasks.md のタスク内容から設計や実装計画を作成します。 事前に要件や仕様を memory-bank/tasks.md に記述した上で、PLANモードを選択して「PLAN MODE」と入力すると、計画立てしてくれます。 実行する際は、新しいチャットを開いて行うのがおすすめです。 参考までに memory-bank/tasks.md の内容は以下です。 足りない部分はチャットしながら補う前提でざっくりとした記述となっています。 memory-bank/tasks.md を表示 ## 要件・仕様 - OSごとに強制アップデートのバージョンを指定できる - 強制アップデート条件 - サーバーで設定されている強制アップデートバージョン > アプリバージョン ## 設計 - アプリ - 全APIのリクエストヘッダーに下記情報を付与 - OS:XX-XXX-XXXX - `iOS` - `Android` - アプリバージョン:XX-XXX-XXXX - `1.0.0` - サーバーからHTTPステータスコード427が返却された場合に強制アップデートのダイアログを表示する - サーバー - アプリ向けの全APIにおける共通処理のミドルウェアとして実装 - リクエストヘッダーで必要な情報を受け取る - OS:XX-XXX-XXXX - アプリバージョン:XX-XXX-XXXX - OSもしくはアプリバージョン情報がリクエストヘッダーにない、もしくは不正だった場合はエラー - HTTPステータスコード 400? - OSおよびアプリバージョン情報をもとにバージョン判定を行う - 強制アップデート対象の場合 HTTPステータスコード427を返却 mermaid flowchart TD A[バージョン要件を判定するミドルウェア] --> B[HTTPリクエストヘッダーからアプリのバージョン情報を取得] B --> C{バージョン情報が有効か} C -- No --> D[エラー HTTPステータスコード 400 返却] C -- Yes --> F[HTTPリクエストヘッダーから端末のOS情報を取得] F --> G{バージョン情報が有効か} G -- No --> D G -- Yes --> H{バージョン情報とOS情報から強制アップデート対象判定} H -- No --> J[ミドルウェア終了。次の処理に進む。] H -- Yes --> I[エラー HTTPステータスコード 427 Require Update 返却] subgraph バージョン判定ロジック K[OS情報からサーバーで定義されているiOSもしくはAndroid のminVersion を取得] K --> L{アプリバージョン < minVersion} L -- No --> M[Return False] L -- Yes --> N[Return True] end H --> |バージョン判定ロジック| K 入力内容はこちら。 以下が実行結果です。今回は1回の指示のみで詳細な計画を立ててくれました。 memory-bank/ 配下のファイルを詳細に記述しておくなど、前段の準備をしっかりしておくことで、チャットへの入力はほとんど無く詳細な結果を返却してくれます。 PLANモード 実行結果を表示 # 強制アップデート機能 実装計画(PLAN MODE) ## 1. 要件分析(Requirements Analysis) - [x] OSごとに強制アップデートのバージョンをサーバー側で指定できる - [x] サーバーで設定された強制アップデートバージョン > アプリバージョンの場合、強制アップデートを要求 - [x] 必要なリクエストヘッダー(RA-Device-OS, RA-App-Version)がない/不正な場合は400エラー - [x] 強制アップデート対象の場合はHTTP 427(Require Update)を返却 ### 技術的制約 - [x] サーバー側は全APIに対して共通ミドルウェアとして実装 - [x] バージョン情報・OS情報はリクエストヘッダーで受け取る - [x] サーバー側でOSごとのminVersionを管理 --- ## 2. コンポーネント分析(Component Analysis) ### 影響範囲 - APIサーバー(Laravel想定) - 共通ミドルウェア層(全APIに適用) - バージョン管理設定(config or DB) - エラーハンドリング(400, 427) - クライアント(アプリ) - ヘッダー送信 - 427受信時のダイアログ表示(※サーバー側計画外) ### 変更点・依存関係 - ミドルウェア新規作成 - リクエストヘッダー検証 - バージョン判定ロジック - エラー/強制アップデート応答 - 設定管理 - OSごとのminVersionをどこで管理するか(configファイル or DB) - テスト - 単体テスト(ミドルウェア) - 統合テスト(API経由) --- ## 3. 設計判断(Design Decisions) ### アーキテクチャ - [x] LaravelのHTTPミドルウェアとして実装 - [x] 設定は `config/force_update.php` などで管理(初期はファイル、将来的にDB化も考慮) - [x] バージョン比較はSemver互換の比較ロジックを実装 ### UI/UX - [ ] サーバー側は非該当(アプリ側で427時にダイアログ表示) ### アルゴリズム - [x] バージョン比較ロジック(Semver対応) - [x] ヘッダー検証ロジック --- ## 4. 実装戦略(Implementation Strategy) ### フェーズ1: ミドルウェア実装 - [ ] ミドルウェア新規作成(例:`ForceUpdateMiddleware`) - [ ] ヘッダー検証 - [ ] バージョン判定ロジック実装 - [ ] 設定ファイル作成(`config/force_update.php`) ### フェーズ2: エラー・強制アップデート応答 - [ ] 400, 427レスポンスの実装 - [ ] エラーハンドリングの統一 ### フェーズ3: テスト・ドキュメント - [ ] 単体テスト(ミドルウェア) - [ ] 統合テスト(API経由) - [ ] API仕様書・README更新 --- ## 5. テスト戦略(Testing Strategy) - [ ] ヘッダーが正しい場合/不正な場合のテスト - [ ] バージョンがminVersion未満/以上のテスト - [ ] OSごとの分岐テスト - [ ] 400, 427レスポンスの検証 - [ ] テストカバレッジの確認 --- ## 6. ドキュメント計画(Documentation Plan) - [ ] API仕様書(リクエストヘッダー、エラーコード追加) - [ ] 実装手順・設定方法のREADME追記 - [ ] テストケース一覧 --- ## 7. クリエイティブフェーズ要否(Creative Phase Identification) - UI/UX: **不要**(サーバー側のみ) - アーキテクチャ: **必要**(設定管理方法、バージョン比較ロジック設計) - アルゴリズム: **必要**(Semver比較) --- ## 8. チェックリスト(Verification Checklist) - [x] 要件がすべて網羅されている - [x] 影響範囲・コンポーネントが明確 - [x] 設計判断・実装戦略が明記されている - [x] テスト・ドキュメント計画がある - [x] クリエイティブフェーズ(アーキテクチャ・アルゴリズム)が明示 --- ### 次モード推奨 - **アーキテクチャ設計・バージョン比較ロジックの詳細設計が必要なため、次は「CREATIVE MODE」推奨。** CREATIVEモード PLANモードで、アーキテクチャ設計・バージョン比較ロジックの詳細設計が必要ということだったので、CREATIVEモードを用いてその作業を行います。 今まで同様に、CREATIVEモードを選択して「CREATIVE MODE」を入力します。 この際に、claude-3.7-sonnet の Thinking を設定することでより高精度な設計を行ってくれます。 CREATIVEモード 実行結果を表示(一部) # 強制アップデート機能 バージョン比較アルゴリズム設計 🎨🎨🎨 ENTERING CREATIVE PHASE: ALGORITHM 🎨🎨🎨 Focus: 強制アップデート機能におけるバージョン比較ロジック Objective: 正確かつ効率的なバージョン比較アルゴリズムを設計する Requirements: - 基本的なバージョン比較(X.Y.Z形式) - リクエストヘッダーから取得したバージョン文字列の検証 - パフォーマンスを考慮した実装 - シンプルな実装を重視 ## コンテキスト ### アルゴリズム要件 - バージョン形式: `X.Y.Z`(メジャー.マイナー.パッチ)の形式のみ - 無効なバージョン文字列の検出と処理 - 単純なバージョン比較(新しいか古いか) ### 技術的制約 - PHPでの実装 - 高速な比較処理(全リクエストで実行されるため) - LaravelミドルウェアからのAPIとしての利用しやすさ ## バージョン比較アルゴリズムのオプション ### オプション1:独自実装(カスタムロジック) - **説明**: PHPの文字列操作と数値比較を用いた独自実装 - **手法**: 1. バージョン文字列を`.`で分割 2. メジャー、マイナー、パッチそれぞれを数値比較 - **長所**: - 完全にカスタマイズ可能 - 外部依存がない - 特定のユースケースに最適化可能 - **短所**: - 実装の複雑性 - エッジケースの見落としリスク - メンテナンスコスト - **計算量**: O(n)(nはバージョン文字列の長さ) - **実装複雑性**: 中 ### オプション2:既存のライブラリ活用(composer/semver) - **説明**: Composerが提供する`composer/semver`パッケージを使用 - **手法**: 1. `composer/semver`をインストール 2. `Comparator`クラスの`lessThan()`/`greaterThan()`メソッドを使用 - **長所**: - 広く使われており信頼性が高い - SemVerの仕様に準拠 - メンテナンスの心配がない - **短所**: - 外部依存関係の追加 - 多少のオーバーヘッド - シンプルなケースには過剰な機能 - **計算量**: ライブラリ内部実装に依存 - **実装複雑性**: 低 ### オプション3:PHP標準関数 version_compare - **説明**: PHPの標準関数`version_compare()`を使用 - **手法**: 1. `version_compare($version1, $version2, $operator)`を呼び出し 2. 結果に基づいて判定 - **長所**: - PHPに標準搭載 - 追加依存関係が不要 - シンプルなAPI - 高速な処理 - **短所**: - 複雑なバージョン比較ケースには不向き(今回は必要なし) - **計算量**: O(n)(PHPの内部実装による) - **実装複雑性**: 非常に低 ## 決定 - **選択したオプション**: オプション3(PHP標準関数 version_compare) - **理由**: - PHPに標準搭載されており外部依存関係が不要 - シンプルで高速な実装が可能 - 基本的なX.Y.Z形式のバージョン比較に十分対応 - 追加のパッケージ導入が不要でプロジェクトが軽量化 - 今回の要件に最適なシンプルさを提供 - **実装ガイドライン**: // バージョン比較 public function isUpdateRequired(string $clientVersion, string $requiredVersion): bool { return version_compare($clientVersion, $requiredVersion, '<'); } // バージョン検証(X.Y.Z形式のみ許可) public function isValidVersion(string $version): bool { // X.Y.Zの形式のみ許可する単純な正規表現 $pattern = '/^\d+\.\d+\.\d+$/'; return preg_match($pattern, $version) === 1; } ## 検証 - 要件の充足: - [✓] 基本的なバージョン比較(X.Y.Z形式) - [✓] バージョン文字列の検証 - [✓] パフォーマンスへの配慮 - [✓] シンプルな実装 - 技術的実現性: 高(PHP標準関数を使用) - リスク評価: 低(PHPの標準機能に依存) 🎨 CREATIVE CHECKPOINT: アルゴリズム設計確定 - 進捗: バージョン比較ロジックの決定完了 - 決定: PHPの標準関数version_compareと単純な正規表現を使用 - 次のステップ: ForceUpdateMiddlewareの実装 🎨🎨🎨 EXITING CREATIVE PHASE 🎨🎨🎨 Summary: バージョン比較アルゴリズムとして、シンプルさと実装の容易さを考慮してPHPの標準関数`version_compare`を採用。基本的なX.Y.Z形式のバージョン検証のみを行う単純な実装アプローチを決定。 Key Decisions: - PHPの標準関数version_compareを使用してバージョン比較を実装 - シンプルな正規表現によるX.Y.Z形式のみの検証 - 外部依存関係を減らしたシンプルな実装 Next Steps: - ForceUpdateMiddlewareへの実装 - 単体テストによる検証 複数の設計パターンを検討し、採用した設計の意思決定の内容まで記述してくれました。 IMPLEMENTモード 以上の実装計画と設計をもとに実装します。 IMPLEMENTモードを選択して「IMPLEMENT MODE」と入力すると実装に進みます。 ここで新しいチャットを開いて実行すると良いでしょう。 結果は長いので割愛しますが、1回のやり取りでは全て完璧な実装とはならず、テストまで全て成功するまでには何度かやり取りを繰り返しました。 ただ、計画のチェックリストに自動的にチェックをつけながら実装してくれるため、内容の網羅性が高く、また、効率的なやり取りを行えると感じました。 まとめ 今回は簡単な機能実装を例に Cursor Custom modes を利用した cursor-memory-bank のワークフローを試しました。 小さい内容のタスクであったため効率的に各フローを実行できましたが、もっと大きなプロジェクトで大きな課題に対して実行するとまた違った結果になるかと思います。また、今後も試してみたいと思います。 今回紹介した手法は、あくまで現時点でのアプローチの一例に過ぎません。重要なのは、自分たちのワークフローや開発スタイルに合わせて適切にカスタマイズし、継続的に改善していくことだと思います。 本記事が少しでも参考になれば幸いです。ありがとうございました!
Amazon CloudWatch Logs Insights を使ったログ調査〜最新機能を添えて まえがき 背景 Cloud Watch Logs Insightsを使った調査事例 最新のCloud Watch Logs Insights事情 新機能1:AIアシスタントによるクエリ自動生成 日本語プロンプトの制限事項 英語プロンプトの例(日本語文字列あり:エラー発生) 複雑なプロンプト例(エラー発生) 新機能2:OpenSearchSQLクエリ 総括 最後に こんにちは、開発本部 RetailHUB開発部 NetSuperグループ兼、CTO 室 Dev Enable グループに所属するフルスタックエンジニアをやらせていただいています、ホーク🦅アイ👁️です。AWS Summit2025が来月に開催されますがいかがお過ごしでしょうか。 aws.amazon.com まえがき 弊社ではつい先だってプレスリリースされました、Cursorエディタの全エンジニア配布が社内で最もインパクトのある出来事となっており開発部内でもSlackで専用のチャンネルを用意して会話をしたり、社内勉強会の開催も近々予定していたりと大いに盛り上がっております。 prtimes.jp 背景 昨年、私が執筆したブログ記事でAmazon Data Firehoseについて触れさせていただきましたが、データウェアハウスなログ管理・分析手法として現在も所属チームで稼働中です。 tech.every.tv 一方、別のインフラ構成で稼働しているGraphQLシステムがありこちらはAWS Lambdaを導入しているだけでそのログは標準のAmazon CloudWatch Logsに全依存しているのが現状です。そんな中、とある調査が必要になりこのLambdaのロググループの大量のログを検索しなければならないことになったのです。構造化ログと非構造化ログが混在しておりS3に保存もしていないので過去のログを今すぐにAthenaを使ってクエリ発行もできないため仕方なくLogs Insightsを使って直接検索することになりました。Logs Insights自体は2018年にGAとなっているレガシー技術ですが、実は直近で大きな新しい機能が追加となっていたのでそれを紹介しつつ、本調査の体験談をさせたいただきます。 Cloud Watch Logs Insightsを使った調査事例 先ほど述べたLambdaログを調査するにあたり、現状システムのログ出力前提は以下のようになっています。 APIプログラム内で出力されるログの種類は、処理途中の変数値やタイムスタンプを記したinfoログと例外発生時のエラーログがあり、その2種類が同じロググループに混在している(構造化・非構造化ログの混在) 調査対象のAPIはログイン後に特定のユーザ(スマホアプリ)が叩くものだが一部のログにしかユーザIDが埋め込まれていない エラーログにはそのユーザIDは埋め込まれていない STG環境とPRD環境のログが同じロググループに混在している この前提条件の下、1リクエストで出力させたログをまとめて、ある期間中にどのユーザが特定エラーを起こしたのか、またその件数はいくつなのか、何人がエラーを起こしたのかを抽出したいという要件がありました。 これらを踏まえてChatGPTと壁打ちの結果、なんとかLogs Insights QLのクエリ構文を完成させることができ、期待するデータを抽出することができました。クエリ例は以下のようになります。 fields @ timestamp , @message | parse @message " *\t*\t*\t* " as level , timestamp , request_id, body | parse body / ' user_id ' :\s* ' (?<usr_id>[^ ' ]+) ' / | parse body / ' shop_id ' :\s* ' (?<shp_id>[^ ' ]+) ' / | parse body / ' db_name ' :\s* ' (?<db_nme>[^ ' ]+) ' / | parse body / ' error_message ' :\s* ' (?<err_message>[^ ' ]+) ' / | stats latest(usr_id) as user_id, latest(shp_id) as shop_id, latest(db_nme) as db_name, latest(err_message) as error_message, latest( timestamp ) as log_timestamp by request_id | filter error_message = " 情報が取得できませんでした。 " and db_name != " stg_database " 簡単に説明すると、@messageでログ文字列全体を取得して、タブ区切りになっている部分を4分割してLambdaのリクエストを一意に決めるRequestIdを取得しつつ、ログ本体をbodyとしています。このRequestIdでグルーピングすることで1リクエストのログ全体を集約しました。ログは、plaintextの場合もあればJson形式の場合もあったり、またJson形式がネストしていたりとJsonパースを共通ルール化して記述できなかったので単純に正規表現でuser_idなどを抽出しています。最後にPRD環境のみとする条件を追加しています。 最新のCloud Watch Logs Insights事情 CloudWatch Logs では2024年12月、以下の3つの機能がGAとなって可能性が広がっております。 生成AIを用いた自然言語によるクエリ作成・更新機能 OpenSearch Service Piped Processing Language (PPL)のサポート OpenSearch Service Structured Query Language (SQL)のサポート また、2025年7月31日以降は、IAMポリシーで logs:StartQuery,logs:GetQueryResults の権限追加が必須となりセキュリティ向上が見込まれます。 そこで、先ほどのLambdaログ調査で使ったクエリと同様の結果を出すためのSQL記述はどのようなものなのか、生成AIプロンプトでリクエストするとどのようなクエリを生成するのかも気になりました。もしかしたら、もっと簡単な文法で記述できていたのだろうか、GPTとの壁打ちをしてやっとの思いで完成させたクエリだったのでその時間短縮ができるのであれば良い選択肢になると思い試すことにしました。 新機能1:AIアシスタントによるクエリ自動生成 日本語プロンプトの制限事項 Cursorに上記の完成形のクエリを渡して「このクエリ結果と同じになるクエリを生成してもらうためのプロンプトを考えて」とお願いして作成してもらったものを使って実行してみたら以下のエラーになりました。日本語がそもそもダメだったようです。。 # 日本語での入力例(エラーになります) Lambda関数のログから以下の条件で分析をしたいです: 1. ログからuser_id, shop_id, db_name, error_messageを抽出 2. request_idでグループ化し、各フィールドの最新値を取得 3. error_messageが"情報が取得できませんでした。"で、 db_nameが"stg_database"以外のものを抽出 ⚠️ エラーメッセージ Bad Request - Query generation currently only supports prompts in English. Only english characters, numbers and standard punctuations are allowed. 英語プロンプトの例(日本語文字列あり:エラー発生) 日本語には対応していないようなので、英語プロンプトに変更してとCursorにお願いして作成し直してもらいました。それを使って実行するとまたもやエラーになってしまいました。。どうやら、メッセージ文字列そのものも日本語(マルチバイト?)が混じっているとエラーになるようです。 I want to analyze Lambda function logs with the following conditions: 1. Extract user_id, shop_id, db_name, and error_message from logs 2. Group by request_id and get the latest value for each field 3. Filter where error_message is "情報が取得できませんでした。" and db_name is not "stg_database" ⚠️ エラーメッセージ Bad Request - Query generation currently only supports prompts in English. Only english characters, numbers and standard punctuations are allowed. 複雑なプロンプト例(エラー発生) というわけで全て英語のプロンプトをCursorにお願いして作成してもらい、それを使って実行してみましたが、以下のようにまたまたエラーになってしまいました。長文だったり要件が複数・複雑だとエラーが出てクエリ生成に失敗してしまったのでそもそも簡単なクエリ生成にしか対応できないようです。 I want to analyze Lambda function logs with the following conditions: 1. Extract user_id, shop_id, db_name, and error_message from logs 2. Group by request_id and get the latest value for each field 3. Filter where error_message is "Information could not be retrieved" and db_name is not "stg_database" ⚠️ エラーメッセージ Bad Request - The provided query prompt failed to generate a valid query. Try rephrasing the query prompt. その後、簡単な文章プロンプトで少しづつクエリを組み立ていこうと、Generate New Query→Update Query→Update Query...と繰り返していきましたが単純にUpdateでプロンプトの結果を追加する時もあれば上書きする時もあったので中々一筋縄ではいかない結果となりました。。 新機能2:OpenSearchSQLクエリ こちらも同様にCursorを使ってSQLを作ってもらってそれを実行する方法で試しました。確かに簡単なSQL構文では正しく動きましたし、元からある知見をそのまま使ってすぐにクエリ作成ができるのは便利でした。しかし、今回のようなちょっと複雑なクエリ要件になるとCursorが生成したSQL(以下、一例)では文法NGだったり実行できてもヒット数0件だったりと何度か壁打ちして試しましたが上手くいきませんでした。。 SELECT request_id, user_id, shop_id, db_name, error_message, `@ timestamp ` as log_timestamp FROM `/aws/lambda/FunctionA` WHERE regexp_extract(`@message`, ' error_message '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) = ' 情報が取得できませんでした。 ' AND regexp_extract(`@message`, ' db_name '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) != ' stg_database ' | fields regexp_extract(`@message`, ' user_id '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as user_id, regexp_extract(`@message`, ' shop_id '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as shop_id, regexp_extract(`@message`, ' db_name '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as db_name, regexp_extract(`@message`, ' error_message '' ?\s*:\s* '' ([^ '' ]+) ''' , 1 ) as error_message, split(`@message`, ' \t ' )[ 3 ] as request_id, `@ timestamp ` | sort `@ timestamp ` desc | limit 1000 ; 総括 本記事では、前半でCloudWatch Logs Insightsを使ったログ調査事例について話しました。後半では、最新機能についても触れました。生成AIによる自動生成はまだまだ改善の余地がありそうな結果でした。SQLの方は構文や関数は既に出揃っているのでそれらを全て理解すれば複雑なクエリ実行も成功することはできると思います。 そもそも、Logs Insights使用はマネーコストが掛かるためこれだけを使ったログ管理運用は最適解ではないかもしれません。そのため使用頻度は高くないと思いますが、今回のようなデータレイクないろんな形式のログが混在しているのを一括して検索可能であったり、複数のLogGroupsを横断して検索可能であったり、CloudWatch Dashboardsでリアルタイム監視ができたり、と使い分けとして使用機会自体はまだまだありますので新しい機能を駆使して快適なログライフを送れると良いと思いました。 また、新しいログ分析手法としてデータレイクとしてApache IcebergをサポートしたS3 Tablesを使ってAthenaで分析するパターンが出てきました。2025年5月執筆時点ではコンソール上でnamespace作成、テーブル作成も可能となっておりました。こちらは既存の弊社システムのログ分析手法をアップデートでき得るものとして後日検証して別途報告できればと思います。 これにて本記事の結びとさせていただきます。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv
はじめに こんにちは、開発本部のデータ&AIチームの蜜澤です。 Amazon QuickSight(以下quicksight)にはハイライト機能がありますが、インタラクティブにランキング表の指定したワードにハイライトすることはできません。 本記事ではquicksightのハイライト機能を使用せずに、ランキング表の同じワードにハイライトする方法を紹介したいと思います。 本記事はquicksightの機能を一通り知っている方向けの内容になっています。 使用するデータ 今回使用するデータは、レシピ動画サービスの検索データを集計して作成した「にんじんと一緒に検索された回数が多いワードの年度ごと(2021年〜2024年)のランキング」のデータです。 一緒に検索されたワードとは「にんじん しりしり」のようにスペース区切りで共に検索されたワードのことを指します。 quicksightの「テーブル」ビジュアルを使用してランキング表を表現するために、rankカラムと各年度のカラムを作成し、各年度のカラムには一緒に検索されたワードを格納します。 ※データはサンプルなので実際の傾向とは異なります。 実際にquicksightでテーブルを作成すると上記画像のようなテーブルが作成できます。 ハイライトをつける 先ほどのランキング表を見てみると、「しりしり」の順位が2021年から2024年にかけて上がっていることや、「大根」の順位が2021年から2024年にかけて下がっていることがわかります。 しかし、この表で「しりしり」や「大根」の順位の遷移を目で追うのは大変なので、特定のセルのみに色をつける(ハイライトする)というのが、今回の目標になります。 1.パラメータを作成 以下のようなパラメータを作成します。 このパラメータには、ハイライトしたいワードが入リます。 2.計算フィールドを作成 以下のような計算フィールドを作成します。 2021年カラムの値が、Highlightパラメータと同じ場合は1、異なる場合は0を返す計算フィールドになっており、この値によって後述する条件付き書式を設定します。 同様にして2022~2024用の計算フィールドも作成します。 3.条件付き書式を設定 プロパティ -> 条件付き書式設定 -> 列で「2021」を選択 -> 背景色を追加する で以下のように設定します。 条件の対象フィールドには先ほど作成した対応する計算フィールドを選択し、条件は「次と等しい」、値は1にします。 2022~2024にも同じ設定をします。 4.コントロールを追加 1.で作成したパラメータを以下の設定でコントロールに追加します。 5.完成 必要な工程は1~4になります。 作成したコントロールにハイライトしたいワードを入力すれば、ランキング表に色がつきます! ハイライトがつくことで順位の遷移が見やすくなりました! まとめ 本記事では、quicksightで特定のセルにハイライトをつけることができるランキング表の作成方法を紹介しました。 この記事がいつか誰かの助けになれば幸いです! 最後まで読んでいただきありがとうございました。
はじめに 背景 ADRについて ADRの始め方 ADRの実践例 ADRの具体例 おわりに はじめに デリッシュキッチンのiOSアプリ開発を担当している池田です。今回はiOSチームでADR(Architecture Decision Record)を用いてチームの意思決定の記録を残し始めた話をします。正確には個人でADRを記録していたものの、チームでの運用はしていなかったため、この機会にチームでの管理を開始しました。 背景 デリッシュキッチンのiOS開発は当初少人数で行っていたため、実装に関する詳細なドキュメントを作成するという習慣がありませんでした。しかし近年、メンバーの入れ替わりや新入社員の参加により、設計や実装の経緯を説明する機会が増えてきました。これを良い機会と捉え、私が個人で記録していたADRをチームに共有することにしました。 ADRについて ADRとは「Architecture Decision Record」の略で、プロジェクトにおける重要な意思決定を記録するためのドキュメントです。チームでの決定事項とその経緯が不明確になり、都度確認が必要になったり、経緯を知る人が退職してしまったりした経験は多くの方があるでしょう。ADRを残すことで、このような意思決定プロセスの消失を防ぐことができます。 意思決定は時間とともに消失する ADRにまとめる主な項目は以下の通りです。 タイトル:何についての決定か簡潔に表現 ステータス:提案中、承認済み、廃止、変更など コンテキスト:その決定が必要になった背景や状況 決定事項:採用した選択肢とその理由、検討した代替案 結果:この決定による影響(メリット・デメリット) ADRの始め方 ADRを始めるにはいくつかのステップがあります。 テンプレートの作成と合意 - 最小限の項目でシンプルに始めることが重要です。チームで合意したフォーマットがあれば、一貫性を維持しやすくなります。 記録場所の決定 - アクセスしやすく、気軽に編集できる場所が理想的です。GitHubやConfluenceなど、チームが日常的に使用するツールが最適です。 最初のADRの作成 - 完璧を目指すよりも、まずは記録することを優先しましょう。 ADRの実践例 私たちのチームではGitHub DiscussionにADRをまとめることにしました。everyではConfluenceを利用しているため、そこに記載する選択肢もありましたが、開発関連の内容とその他の情報を分離する目的からGitHub Discussionを選択しました。 ただ、振り返ってみると、PdMやデザイナーなどの非エンジニアとの共有や、複数リポジトリに対するADRの一元管理の観点からは、Confluenceでの管理の方が適していたかもしれません。 私たちのチームでは前述の5項目をベースとしたシンプルなテンプレートを作成しました。タイトルには決定事項を記載し、ステータスにはGitHub Discussionのカテゴリー機能を活用することにしました。実際のテンプレートは以下の通りです。 # 経緯 # 提案内容 # 承認した場合の結果 ## メリット ## デメリット # 備考 最初のADRはサンプルの意味合いも込めて「ADRを使って意思決定事項を残す」というテーマで作成しました。 ADRの具体例 実際のADRの例 おわりに デリッシュキッチンのiOSチームではADRの記録を始めました。これにより、新しいメンバーが参加した際にもスムーズに開発を進められるようになると考えています。 また、近年ではAIを活用した開発が進んでいますが、明確なドキュメントがあることでAIに対しても適切な指示を出しやすくなり、プロジェクトの文脈を理解した上でより質の高い提案を得られるようになるでしょう。 重要なのは完璧を目指すのではなく、まずは小さく始めて習慣化することです。最初は簡単な記録から始め、徐々に洗練させていけば十分です。意思決定はコードと同様に重要な資産であり、チーム全体で共有・管理することで、長期的な開発効率と品質向上につながります。 皆さんもぜひADRを取り入れ、誰もが開発しやすい環境を整備していきましょう。
はじめに こんにちは。デリッシュキッチン開発部の村上です。 弊社ではエンジニアとPdM全員にCursorを配布しており、生成AIを活用した開発を積極的に行っています。 prtimes.jp エンジニア組織では開発生産性10倍を目標としていますが、そこに到達するためには新しい技術やツールに触れながら、 ある意味でこれまでの開発のやり方を根底から疑ってみて、生成AIの活用を前提とした新しい組織・業務設計をしていく大胆さが求められていきます。 そんな中で、私の所属する組織ではちょうどLPの開発について考える機会がありました。 LPはそこまで複雑な機能はなく、基本的にはデザインに基づいたコーディングが中心になりますが、地味に時間がかかります。 そこで、最近話題のFigma-Context-MCPを使った検証をしているのでそこで得た知見を紹介しようと思います。 今月の初めには弊社CTOもMCPサーバーに関する記事をあげているのでぜひこちらもご覧ください。 tech.every.tv Figma-Context-MCPとは Figma-Context-MCPとは、FigmaのデザインデータをCursorなどのAIコーディングツールの "ツール呼び出し" として扱えるようにする Model Context Protocol (MCP) サーバーです。Figma APIから取得した複雑なJSONの情報を整理してLLMに橋渡しすることによって、効率的にデザイン解析やコード生成が可能になります。 github.com 始め方 セットアップは簡単で公式の クイックスタート を見ればすぐに利用することができますが、こちらでも簡単に説明します。 Figmaアクセストークンの取得 Figma-Context-MCPはFigma APIを通してFigmaの情報にアクセスするのでアクセストークンが必要になります。 アクセストークンはアカウントの設定メニューからSecurityタブを開けば、アクセストークン発行の導線が見つかります。公式ドキュメントによると File content と Dev resources の読み取り権限が実行に必要なため注意してください。 Cursor側でMCPサーバーを設定 こちらも公式ドキュメントからほとんど変えていないですが、 .cursor/mcp.json にMCPサーバーの設定を追加します。 (※2025年4月27日時点でバージョン指定をしていないと接続エラーとなるため、最新のv0.2.1を指定しています) { " mcpServers ": { " Framelink Figma MCP ": { " command ": " npx ", " args ": [ " -y ", " figma-developer-mcp@0.2.1 ", " --figma-api-key=<API KEY> ", " --stdio " ] } } } シンプルなLPデザイン構成でコード生成を試す まずは実際に弊社内でデザインされているLPのFigmaデータから部分的に切り出してシンプルな画像要素中心のLPで検証してみます。 今回は題材として、デリッシュキッチンが手掛けている冷凍宅配弁当の「 Meals 」のデザインデータを使います。 Figmaの構造 切り出したFigmaのLPデータは以下のような構造で画像が中心となっているため、とてもシンプルです。 test1(Frame) ├── Group 48(Group) │ └── kv(Group) │ ├── 帯(Group) │ │ ├── このページをご覧の方 限定!(Text) │ │ └── Rectangle 40(Rectangle) │ └── kv_food(Image) └── diet_onayami(Image) LP生成 Cursorから指示を出してみます。Figmaでは「Copy Link to Section」という形でセクションのリンクを取得できるのでそのリンクを使用します。 Figmaでページ全体を共有すると複数デザインがおいてある場合にすぐにContext上限で応答パフォーマンスも悪くなるため、生成したいものだけに絞ることをお勧めします。 以下のfigmaのデザインを参照して、コーディングを行ってください。 @<figmaの該当セクションのURL> デザインと生成されたLPのスクショを比較したのがこちらです。 ヘッダーの位置がずれてしまっていたり、marginが余計に入ったりと満足のいくものが出てきません。 Auto Layout機能を使ってレイアウトを認識させる 調べていくと公式ドキュメントの Best Practices で以下の言及がされていました。 Use auto layouts—the MCP isn't great at handling floating or absolutely positioned elements just yet どうやらMCPはGroupで指定された絶対値参照のレイアウトが得意ではないようでAuto Layoutを使うことが推奨されていました。私はFigmaについては初心者なので詳しいわけではないですが、Auto LayoutはCSSでいうflexboxのように要素を規則的に並べることが出来る機能で要素の並び方や要素間のgapを定義することができます。 さっそく、 kv と 全体( test1 )にAuto Layoutを適用して、Figmaを更新してみます。 test1(Auto Layout) ├── Group 48(Group) │ └── kv(Auto Layout) │ ├── 帯(Group) │ │ ├── このページをご覧の方 限定!(Text) │ │ └── Rectangle 40(Rectangle) │ └── kv_food(Image) └── diet_onayami(Image) この状態で先ほどと同じ指示でLP生成を行った結果が以下の画像です。 Auto Layoutを指定しただけでその他は構造も命名も変えていませんが、要素の位置を認識してデザイン通りに配置してくれています。画像中心のレイアウトであれば、ここから縦に数を増やしてもアウトプットの精度は大きく変化せず安定していました。 もう少し複雑なLPデザイン構成でコード生成を試す ほとんどのセクションが画像となっているデザイン構成であれば、前述の方法でもかなりLPの開発をショートカットできそうですが、そういったケースだけではなさそうです。もう少し構成要素が多いものでみてみましょう。 Figmaの構造 今回、Auto Layout設定は一部に設定していますが、要素やその配置は先ほどよりやや複雑です。 test3(Auto Layout) └── nutrition(Frame) ├── title(Group) │ ├── icon_point(Image) │ ├── nutrition_title(Image) │ └── nutrition_title_bg(Image) └── 栄養基準(Auto Layout) ├── Group 31(Group) │ ├── 糖質・脂質・塩分・野菜量など8項目の栄養基準(Text) │ └── Rectangle 25(Rectangle) ├── nutrition_ingredients_img(Image) └── ※上記に合わせて脂質(10~20g)、炭水化物(15~35g)の基準を設定(Text) LP生成 先ほどと同じ指示でLP生成をしてみます。 以下のfigmaのデザインを参照して、コーディングを行ってください。 @<figmaの該当セクションのURL> 生成されたLPをみてみるとデザインに比べて、大きく崩れていることがわかります。 絶対値で重なって構成されている要素は一つの画像として扱う 大きく崩れた部分は以下のtitleのGroup配下の要素でした。 test3(Auto Layout) └── nutrition(Frame) ├── title(Group) ├── icon_point(Image) ├── nutrition_title(Image) └── nutrition_title_bg(Image) 今回のFigmaデザインでは3つの画像で一つのヘッダーセクションを構成していましたが、これは前述したように絶対値を苦手とするMCPではコード生成のクオリティが下がります。可能な限り、こういった要素は一つの画像にまとめると自動生成が楽になります。 同一テキスト間で違う文字サイズ、色を使う場合はテキストごと分ける Figmaでは同一テキストパーツでも途中から別の文字サイズ、色を適用することが可能です。今回だと「糖質・脂質・塩分・野菜量など8項目の栄養基準」の部分はデザインだと「8項目の栄養基準」の部分のテキストサイズが大きく、テキストカラーも緑に変わっています。 しかし、MCPでデザインデータを取得するとその情報がLLMに渡す過程で欠落してしまいます。以下はMCPでのツール応答の該当箇所を抜粋したものです。Figmaとしてはoverrideしたstyle情報は持っているようですが、少なくともv0.2.1のFigma-Context-MCPではそのスタイリングには対応していなさそうです。 children: - id: '5414:5619' name: 糖質・脂質・塩分・野菜量など 8項目の栄養基準 type: TEXT textStyle: style_S5EO4M fills: fill_WTN9RR layout: layout_8PK5H6 text: |- 糖質・脂質・塩分・野菜量など 8項目の栄養基準 globalVars: styles: style_S5EO4M: fontFamily: Noto Sans fontWeight: 700 fontSize: 14 lineHeight: 1.3620000566755022em letterSpacing: 10% textAlignHorizontal: CENTER textAlignVertical: TOP fill_WTN9RR: - '#322012' そこでこうしたデザインをLPに反映させるためにはテキストパーツ自体を分けて配置することで認識することができます。 Figmaの構成を修正してLPを再生成 これら2点を改善できるようにFigmaの構成を見直していきます。 section_titleで1つの画像にする text_wrapper配下でそれぞれスタイルの違うテキストを配置 Figmaの最終構成は以下のようになります。 test3(Auto Layout) └── nutrition(Frame) ├── section_title(Image) └── 栄養基準(Auto Layout) ├── Group 31(Group) │ ├── text_wrapper(Auto Layout) │ │ ├── 糖質・脂質・塩分・野菜量など(Text) │ │ └── 8項目の栄養基準(Text) │ └── Rectangle 25(Rectangle) ├── nutrition_ingredients_img(Image) └── ※上記に合わせて脂質(10~20g)、炭水化物(15~35g)の基準を設定(Text) この状態で再度生成をしてみます。 生成された結果は、上部に無駄なpaddingが存在してしまっていますが、そこだけ消せば、再現度は高そうです。 カルーセル要素のあるLPデザイン構成でコード生成を試す 次にデザインもコードのスタイリングも複雑なカルーセル要素のデザインのコード生成を検証してみます。 Figmaの構成 Auto Layout機能を使って、menu_otherがカルーセルの要素として配置されています。 test5(Auto Layout) └── Frame 74(Auto Layout) ├── その他のメニュー(Text) └── menu(Frame) └── Frame(Auto Layout) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) ├── menu_other(Frame) └── menu_other(Frame) LP生成 LP生成をしてみます。 以下のfigmaのデザインを参照して、コーディングを行ってください。 @<figmaの該当セクションのURL> 生成されたLPをみてみるとそもそもカルーセルを認識できずに縦並びになっています。 意味のある要素名でコードの生成精度を高める 公式ドキュメントの Best Practices でAuto Layoutと一緒に語られているのがFigma上での命名についてです。 Name your frames and groups - Protip: Try Figma's AI to automatically generate names これは単純に名前をつければいいのではなく、LLMが生成するコードのhtml構造やスタイルが正しくなるようにその要素を適切に表した名前にしないといけないということです。 エンジニアであれば、変数やhtmlのclass名に対して気をつかうようにFigmaの要素に対する命名もコードの生成精度に関わってきます。 今回のカルーセルであればカルーセルの要素だとわかるように命名するのが望ましいでしょう。 test5(Auto Layout) └── Frame 74(Auto Layout) ├── その他のメニュー(Text) └── menu(Frame) └── carousel_container(Auto Layout) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) ├── carousel_item(Frame) └── carousel_item(Frame) 命名だけの違いですが、LPを生成してみると以前と変わってカルーセル形式でのスタイリングが適用されます。ただし、2つの修正は必要になります。 item要素の画像の絶対値が認識できないのでitemごと画像化を検討 両側のpadding要素を排除 その他: 安定したコード生成をするためのCursorルール 今回のこれらの検証で何度か繰り返し生成をしていると挙動が不安定なケースがあったので最低限のルールとして./cursor/rulesに以下を配置しました。 ## ガイドライン - get_figma_dataでは必ずnodeIdを指定してください。 - download_figma_imagesが失敗した場合は正しいJSON形式で画像のダウンロードを再度試みてください。 - 画像はbackground要素でない限り、html上でimgタグを使ってください。 get_figma_dataでは必ずnodeIdを指定してください。 何回か指示を出しているとたまに渡したURLのセクション情報を無視して、ファイル指定のみでfigmaのデザインデータを取得しようとします。ファイルにデザインデータが多いとcontext情報が多くてスタックしてしまうので、忘れないようにルールを入れています。 download_figma_imagesが失敗した場合は正しいJSON形式で画像のダウンロードを再度試みてください。 2025年4月27日時点では画像のダウンロードはかなり不安定で「invalid parameter」という形でエラーが頻出します。ダメもとにはなりますが、リトライを明確に指示出しすることで成功確率は少し上がりました。 画像はbackground要素でない限り、htmlのimgタグを使ってください。 たまにfigmaの画像をcssのbackground-imageとして全て参照してしまい、スタイルが大きく崩れることがありました。基本的にはimgタグを使って画像を配置しつつ、命名で background_* と書いてあるときだけはbackground-imageとして使ってもらうなど使い分けると良さそうです。 まとめ まだまだ検証は足りないですが、こうしてFigmaのMCPを使ってみると現状では人間がデザインからコードを起こしているから大きな問題にならない部分でもAIに生成させるとなるとひと工夫必要なケースが多く見えてきました。 今後の業務で活用できるレベルまで到達するにはいかにAIやMCPに相性の良いFigmaのデザイン手法を見つけ、組み立てていけるかを1から考えることが大切になりそうなので、エンジニアもデザイナーもより自分たちの領域を超えて、コミュニケーションを取っていくべきでしょう。 こうした業務での活用に伴う検証は短期的に見れば、非効率で直接書いた方が早いと思う場面は何度もありました。しかし、ここで費やした労力が将来的な開発スピードの大きな差になってくると思うので、弊社では引き続き生成AIを積極的に活用した業務改善にチャレンジしていきます! ぜひ、この取り組みに興味を持った方は一度カジュアル面談でお話しましょう!
はじめに こんにちは。開発部でiOSエンジニアをしている野口です。 今回は挑戦WEEKにてiOS版デリッシュキッチンにウィジェット機能を実装した際の実装方法や、実装中に直面した課題とその解決方法についてお話しします。(※本記事ではiOS版について解説しますが、Android版にも同様の機能を追加しています) 弊社の挑戦WEEKの取り組みについては以下の記事をご覧ください! tech.every.tv ウィジェットについては以前まとめている記事があるので以下の記事もご覧ください! tech.every.tv 今回実装したウィジェットについて 今回実装したウィジェットの要件はこちらです。以下をゴールとして作成しました。 おすすめレシピを表示する レシピは15分毎に更新する ウィジェットをタップするとアプリのレシピ詳細で動画を再生(※タップ後のレシピ詳細の実装は本記事では省略します) 完成したウィジェットはこちらです。 実装の流れ 今回はXcode16.0を使用し、 WidgetKit フレームワークを用いてウィジェットを作成します。 実装の流れとしては以下のようになります。 ウィジェットターゲットの導入 レシピデータの取得とタイムライン管理 ウィジェット画面の作成 1. ウィジェットターゲットの導入 まず、既存のアプリプロジェクトにウィジェット機能を追加するためのターゲットを導入します。 Appleのドキュメント に従い、以下の手順で進めます。 File > New > Target を選択し、 Widget Extension を選択して Next を押します。 プロダクト名を指定します。今回はライブアクティビティやコントロール機能は使用しないため、 Include Live Activity 、 Include Control 、 Include Configuration App Intent のチェックは外して Finish を押します。 注意点: 新たに Target を追加するため、ウィジェットは既存アプリとは別のアプリとして扱われます。そのため、リリース時にはApple Developerサイトでウィジェット用の Identifiers (App ID)を登録する必要があります。 2. レシピデータの取得とタイムライン管理 ウィジェットに表示するデータを取得し、いつ更新するかを管理するのが TimelineProvider の役割です。 ターゲット作成時に、基本的な TimelineProvider のテンプレートコードが生成されます。 // 初期のテンプレートコード(抜粋) struct Provider : TimelineProvider { // データ取得不可時のプレースホルダー表示用データを定義 func placeholder ( in context : Context ) -> SimpleEntry { ... } // ウィジェットギャラリーでのスナップショット表示用データを定義 func getSnapshot ( in context : Context , completion : @escaping ( SimpleEntry ) -> () ) { ... } // ウィジェットの表示更新タイミングとデータ(タイムライン)を定義 func getTimeline ( in context : Context , completion : @escaping ( Timeline < Entry >) -> () ) { ... } } // ウィジェットに表示するデータの単位 (Entry) struct SimpleEntry : TimelineEntry { let date : Date // このデータが表示されるべき日時 let emoji : String // 表示するデータ(初期テンプレート) } 今回実装するにあたり、 TimelineProvider で以下の処理を行います。 placeholder : レシピデータがない場合の表示内容を定義します。(実装詳細は省略) getSnapshot : ウィジェット追加時のプレビュー用に、最初のレシピ情報を取得して表示します。 getTimeline : APIからレシピ情報を取得し、15分ごとに表示内容が切り替わるようなタイムラインを作成します。 以下が TimelineProvider 周りの実装コード全体像です。(API通信処理の実装は省略しています) import WidgetKit import SwiftUI struct Provider : TimelineProvider { // プレビュー用データ取得 func getSnapshot ( in context : Context , completion : @escaping @Sendable ( RecipeTimelineEntry ) -> Void ) { Task { let result = await fetchRecipes() // レシピ情報取得 switch result { case .success( let response ) : if let firstRecipe = response.recipes.first { // 最初のレシピ画像を取得 let fetchedImage = await loadImage(url : firstRecipe.imageURL ) let recipeEntry = RecipeEntry( id : firstRecipe.id , title : firstRecipe.title , image : fetchedImage // 他のレシピ情報も設定 ) completion(RecipeTimelineEntry(date : Date (), recipe : recipeEntry )) // 最初のレシピ情報を反映 } else { // レシピがない場合 completion(RecipeTimelineEntry(date : Date (), recipe : nil )) } case .failure : // APIエラーの場合 completion(RecipeTimelineEntry(date : Date (), recipe : nil )) } } } // タイムライン用データ取得・生成 func getTimeline ( in context : Context , completion : @escaping @Sendable ( Timeline < RecipeTimelineEntry >) -> Void ) { Task { let result = await fetchRecipes() // レシピ情報取得 switch result { case .success( let response ) : if response.recipes.isEmpty { // レシピがない場合は空のタイムラインを返す completion(Timeline(entries : [ RecipeTimelineEntry ( date: Date () , recipe: nil )] , policy : .atEnd)) return } let interval : Int = 15 // 更新間隔(分) let currentDate = Date() // --- レシピ表示の循環性を保つための計算 --- let calendar = Calendar(identifier : .gregorian) let midnight = calendar.startOfDay( for : currentDate ) // 今日の午前0時を取得 // 今日の0時から現在時刻までに経過した分数を計算し、15分間隔で割る // これにより、「今日が始まってから何番目の15分区間か」がわかる let currentTimelineOffset : Int = (calendar.dateComponents([.minute], from : midnight , to : currentDate ).minute ?? 0 ) / interval // --- ここまで --- var entries : [ RecipeTimelineEntry ] = [] // 現在の区間から8つ先まで(2時間分)のエントリーを作成 // ループの開始点を currentTimelineOffset にすることで、getTimeline がいつ呼ばれても // その時点からの適切なレシピが表示されるようにする for timelineOffset in currentTimelineOffset ..< currentTimelineOffset + 8 { // 表示するレシピのインデックスを計算 (リストの末尾まで行ったら先頭に戻る) let recipeIndex = timelineOffset % response.recipes.count let recipe = response.recipes[recipeIndex] // レシピ画像を取得 let fetchedImage = await loadImage(url : recipe.imageURL ) let recipeEntry = RecipeEntry( id : recipe.id , title : recipe.title , image : fetchedImage // 他のレシピ情報も設定 ) // このレシピを表示する日時を計算 (今日の0時から timelineOffset * 15 分後) if let entryDate = calendar.date(byAdding : .minute, value : timelineOffset * interval, to : midnight ) { entries.append(RecipeTimelineEntry(date : entryDate , recipe : recipeEntry )) } } // 生成したエントリーリストと、最後の表示が終わった後に更新するポリシーでタイムラインを作成 completion(Timeline(entries : entries , policy : .atEnd)) case .failure : // APIエラーの場合、とりあえず現在のデータで終了 completion(Timeline(entries : [ RecipeTimelineEntry ( date: Date () , recipe: nil )] , policy : .atEnd)) } } } // 非同期で画像を読み込む private func loadImage (url : URL ) async -> Image? { do { let (data, _) = try await URLSession.shared.data(from : url ) if let uiImage = UIImage(data : data ) { return Image(uiImage : uiImage ) } } catch { print( "Image loading error: \( error ) " ) } return nil } } // 1つのタイムラインエントリーが持つデータ構造 struct RecipeTimelineEntry : TimelineEntry { let date : Date // このデータが表示される日時 let recipe : RecipeEntry? // 表示するレシピ情報 } // レシピ情報の詳細を保持する構造体 struct RecipeEntry { let id : Int64 // レシピID let title : String // レシピタイトル let image : Image? // レシピ画像 (SwiftUI用) // 以下、必要に応じて他のレシピ情報 } getSnapshot の解説 getSnapshot は、ユーザーがウィジェットを追加しようとする際に表示されるプレビュー画面のためのものです。ここではレシピ情報の最初の1件 ( response.recipes.first ) を使ってプレビュー用の SimpleEntry を作成し、 completion ハンドラで返しています。 ちなみにプレビュー表示はこんな感じになります。 getTimeline の解説 getTimeline はウィジェットの表示内容とその更新タイミングを定義します。 課題: レシピの循環表示 要件は「15分ごとにレシピを更新する」ことなので、単純に15分間隔のエントリーをリストの先頭から順に生成することが考えられます。しかし、 WidgetKit のタイムラインの更新タイミングはOSによって最適化されており、必ずしも作成した全エントリー(今回は2時間分)が表示された後に次の getTimeline が呼ばれるとは限らず、OSによる更新が頻繁に起こった場合に、最初の数個のレシピばかりが表示され続ける、という問題が発生し得ます。 解決策: 現在時刻に応じた開始位置の計算 この問題を回避し、レシピを循環的に表示させるため、以下の計算を行っています。 let calendar = Calendar(identifier : .gregorian) let midnight = calendar.startOfDay( for : currentDate ) // 今日の午前0時 (基準点) // 今日の0時から現在時刻までに「15分間の区画」が何回経過したかを計算 let currentTimelineOffset : Int = (calendar.dateComponents([.minute], from : midnight , to : currentDate ).minute ?? 0 ) / interval Calendar を使って「今日の午前0時 ( midnight )」を取得します。これを1日の基準点とします。 dateComponents で、午前0時から現在時刻 ( currentDate ) までの経過時間を「分」で計算します。 その経過分数を interval (15) で割ります。この結果が currentTimelineOffset となり、「今日が始まってから現在時刻までに、15分の区切りが何回あったか」を示します。 タイムラインエントリーの生成 この currentTimelineOffset を使って、 for ループでタイムラインエントリーを生成します。 // 現在の区間 (currentTimelineOffset) から8つ先まで (2時間分) のエントリーを作成 for timelineOffset in currentTimelineOffset ..< currentTimelineOffset + 8 { // レシピリスト内で循環するようにインデックスを計算 let recipeIndex = timelineOffset % response.recipes.count let recipe = response.recipes[recipeIndex] // ... (画像取得、RecipeEntry作成) ... // エントリーが表示されるべき日時を計算 if let entryDate = calendar.date(byAdding : .minute, value : timelineOffset * interval, to : midnight ) { entries.append(SimpleEntry(date : entryDate , recipe : recipeEntry )) } } // 生成したエントリーリストでタイムラインを作成。.atEnd はリストの最後を表示後に更新を促す completion(Timeline(entries : entries , policy : .atEnd)) ループ開始点を currentTimelineOffset にすることで、OSがいつウィジェットを更新しても、その時刻に応じたレシピから表示が始まるように調整されます。これにより、常にリストの先頭からスケジュールが作られてしまうのを防いでいます。 さらに、ループ内で使うレシピ番号は let recipeIndex = timelineOffset % response.recipes.count で計算しています。これによって、レシピリストの最後まで表示したら次は先頭のレシピに戻るように循環的に表示できます。 画像の扱い 画像を表示する際に AsyncImage を使用することができませんでした。なので、 TimelineProvider 側で Image に変換してから SimpleEntry に含めるようにしています。 // 非同期で画像を読み込む private func loadImage (url : URL ) async -> Image? { do { let (data, _) = try await URLSession.shared.data(from : url ) if let uiImage = UIImage(data : data ) { return Image(uiImage : uiImage ) } } catch { print( "Image loading error: \( error ) " ) } return nil } ウィジェット画面作成 ウィジェットのUIは SwiftUI を使って構築します。 まず、 Widget プロトコルに準拠した構造体 (例: RecipeWidget ) でウィジェット全体の設定を行います。 ここで、 .supportedFamilies モディファイアを使って、サポートするウィジェットのサイズ(今回は .systemSmall と .systemMedium )を指定します。 また、 .contentMarginsDisabled() でデフォルトもマージンを無効して画面いっぱいに画像が表示できるようにしています。 struct RecipeWidget : Widget { let kind : String = "RecipeWidget" var body : some WidgetConfiguration { StaticConfiguration(kind : kind , provider : Provider ()) { entry in RecipeWidgetEntryView(entry : entry ) .containerBackground(.fill.tertiary, for : .widget) } .contentMarginsDisabled() // ウィジェットのデフォルトマージンを無効にする .configurationDisplayName( "本日のおすすめレシピ" ) .supportedFamilies([.systemSmall, .systemMedium]) // 対応サイズを指定 } } 次に、実際にウィジェットの内容を表示する View (例: RecipeWidgetEntryView ) を作成します。 サイズ取得: @Environment(\.widgetFamily) を使って、現在表示されているウィジェットのサイズ ( .systemSmall など) を取得します。 データ受け取り: var entry: Provider.Entry のように TimelineProvider から渡されたデータ( SimpleEntry )を受け取るプロパティを定義します。 Linkでタップ時の動作を設定: Link を使ってウィジェット全体または一部をラップし、タップされた際に指定した URL スキーム ("アプリのURLスキーム://レシピ詳細パス/(recipe.id)") を使ってアプリ本体の特定画面へ遷移させます。 サイズに応じて表示を切り替え: family に応じてSmall、Mediumウィジェットに切り替える struct RecipeWidgetEntryView : View { @Environment ( \.widgetFamily ) var family // サイズ取得 var entry : Provider.Entry // 2. データ受け取り var body : some View { if let recipe = entry.recipe { // 3. Linkでタップ時の動作を設定 Link(destination : URL (string : "アプリのURLスキーム://レシピ詳細パス/ \( recipe.id ) " ) ! ) { // 4. サイズに応じて表示を切り替え if family == .systemSmall { SmallWidgetView(recipe : recipe ) } else { MediumWidgetView(recipe : recipe ) } } } } } struct SmallWidgetView : View { let recipe : RecipeEntry // ... Smallサイズ用のレイアウト ... var body : some View { Text( "Medium: \( recipe.title ) " ) } // 省略 } struct MediumWidgetView : View { let recipe : RecipeEntry // ... Mediumサイズ用のレイアウト ... var body : some View { Text( "Medium: \( recipe.title ) " ) } // 省略 } このように、 Widget 構造体で全体設定を行い、表示用の View でデータやサイズに応じたUI構築することで、ウィジェット画面を作成します。 終わりに 今回の挑戦WEEKでのウィジェット機能追加により、ユーザーはアプリを起動せずともホーム画面で手軽におすすめレシピをチェックできるようになりました。これによってアプリの起動率の向上に繋がることを期待しています。 ウィジェット開発は、タイムライン管理など通常のアプリ開発とは異なる考慮点がありましたが、この挑戦を通じてWidgetKitの知見を深めることができ、今後の開発にも活かせる貴重な経験となりました。 今回実装したウィジェット機能は既にリリース済みですので、デリッシュキッチンをお使いの方は、ぜひホーム画面に追加して試してみてください! 最後までお読みいただき、ありがとうございました。
レバテックさんに弊社テックブログを紹介してもらいました 嬉しいニュース テックブログの価値 おすすめ記事のご紹介 バックエンド開発 フロントエンド開発 インフラ・クラウド アプリ開発 AI技術 機械学習 今後の展望 最後に こんにちは、開発本部 RetailHUB開発部 NetSuperグループ兼、CTO 室 Dev Enable グループに所属するホーク🦅アイ👁️です。 嬉しいニュース 先日、嬉しいニュースが届きました。レバテック株式会社様が運営する「レバテックフリーランス」というプラットフォームで、弊社のテックブログを紹介してもらいました! freelance.levtech.jp この記事では、エンジニアのスキルアップに役立つ様々な企業のテックブログが紹介されています。弊社以外にも、多くの企業のブログが掲載されており、技術情報の宝庫となっています。 テックブログの価値 テックブログは、単なる技術情報の共有だけでなく、各企業の開発文化や課題解決のアプローチを垣間見ることができる貴重なリソースです。弊社のブログも、そんな価値を提供できるよう心がけています。 弊社のテックブログは、実務に即した具体的な内容が中心で、バックエンドからフロントエンド、インフラ、アプリ開発、機械学習など多岐にわたる記事を高頻度で投稿しています。毎週2回のブログ公開を目標に、最新の技術動向や実践的な知見を共有しています。 特に、AI技術の活用は弊社の重要な戦略の一つであり、今後も積極的に情報発信していく予定です。 おすすめ記事のご紹介 弊社のテックブログから、各分野のおすすめ記事をご紹介します! バックエンド開発 tech.every.tv tech.every.tv フロントエンド開発 tech.every.tv tech.every.tv インフラ・クラウド tech.every.tv tech.every.tv アプリ開発 tech.every.tv tech.every.tv AI技術 tech.every.tv tech.every.tv 機械学習 tech.every.tv tech.every.tv 今後の展望 このような形で外部から評価いただけたことは、弊社のテックブログ活動の励みになります。今後も、より質の高い技術情報を発信し続けていきたいと考えています。 読者の皆様には、引き続き弊社のテックブログをお楽しみいただければ幸いです。また、ブログを通じて弊社に興味を持っていただけた方は、ぜひ採用情報もご覧ください。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv
データ&AIチームでデータエンジニアを担当している塚田です。 はじめに エブリーではデータ基板の活用の方法としてRedashとAmazon QuickSightを利用しています。 ビジネス職でもSQLを使ったデータ取得・分析は一定程度できる状況ではありますが、まだまだ利用するにあたって壁があることも事実です。 今回、Amazon QuickSightに搭載されている生成AIアシスタントである Amazon Q に新機能『シナリオ分析』がGAされました。 本記事では、このAmazon Q in QuickSightの シナリオ分析機能 にフォーカスし、弊社サービスのデリッシュキッチンの検索データを例にその可能性を検証します。 Amazon QuickSight とは? Amazon QuickSight は、AWSが提供するスケーラブルでサーバーレスなクラウドベースのビジネスインテリジェンス(BI)サービスです。 様々なデータソースに接続し、インタラクティブなダッシュボードやレポートを簡単に作成・共有できます。特徴として従量課金モデルで、インフラストラクチャの管理が不要な点にあります。 Amazon Q とは? (QuickSight 文脈で) Amazon Q は、AWSが開発した生成AIアシスタントです。ドキュメントの要約やコード生成など、様々な業務を支援する機能を持っています。QuickSightにおける Amazon Q は、この生成AI機能をデータ分析に応用したものです。 具体的には、QuickSight 上で「去年の売上トップ3の製品は?」「先月と比較してコンバージョン率はどう変化した?」といった自然言語で質問すると意図を理解し、適切なデータ分析を行い、グラフや表、あるいは文章によるサマリーとして結果を返してくれます。 シナリオ分析がなぜ必要か データを元にした分析は仮説を立てて必要なデータを集め分析し結果を出力し検証するという流れを何回も繰り返すことで実施していきます。 人間ではそれぞれのフェーズごとに時間をかけタスクを進めていきますが、 生成AIを活用した分析だと仮説を立てた時に「もし広告費をxx%増やしたら...」という曖昧な内容を「広告費というメトリクスを」「現在の値からxx%増加させる」という仮説の条件を特定し、その内容に基づいてデータやその関連性を判断し適切なデータの利用や集計を行っていきます。 また、集計したデータのシミュレーションや結果の視覚化まで幅広い対応を行うことができ、得たい結論までの道筋を自動的に出力することによって分析の幅は広がると思っています。 このような仕組みを活用することでアイデアを素早く検証したり既存プロセスの効率化が図れるようになってくると考えています。 実際に利用してみる ※今回ご紹介する機能は執筆時点で利用可能な権限やリージョンに制限がありますので、利用環境を確認した上で利用してください。 該当機能が利用可能な状態のときは左メニューに Scenarios のメニューが表示され、これを選択すると右上に NEW SCENARIO のボタンがあるのでそこから新しいシナリオ分析を開始できます。 その後利用するデータを選択します。すでにQuickSightを利用していればダッシュボードのデータを使うこともできますが、手元にあるCSVなどのデータを利用することも可能です。 今回は弊社アプリのデリッシュキッチンの検索ログの集計済みデータを用いて分析してみたいと思います。 このデータは日毎の検索ワード、検索回数、1日の検索回数などが入っているデータとなります。 簡単な集計の質問を投げてみる このように質問を投げることで英語での回答になりますが、質問に近しい分析メニューを提示してくれます。 今回は質問と同じ内容の Show the top 20 most searched words in the last 7 days を選択します。 選択するとベースのデータに対しての分析方法とともに結果が出力されます。 ※元となったデータを手動で集計したところ同様の結果となっています また、その結果に対して追加の分析も行うことが可能で 豚肉の日毎のトレンド を追加分析すると以下のような結果となりこちらも元データと一致しています。 ※データはサンプルとなるので、実際の傾向とは異なります 同じデータを用いた分析として 過去 1 週間で最も検索された上位 5 つの検索ボリュームの傾向 を出力しました。 ※データはサンプルとなるので、実際の傾向とは異なります このようにStep1からStep5の処理を経て結論まで出力されました。 分析スキルがない方でも利用したいデータが手元にあれば分析はもちろん関連した追加の示唆だしが行える状態になります。 まとめ 今回はQuickSightを例に手軽さと活用範囲の大きさを説明しましたが、これに限らず多数のツールが世の中には出ている状況です。 こういったツールにより、データ分析の専門家ではないビジネスユーザーでも、知りたい情報を迅速に、多角的に探索できるようになると考えています。 アイデアを素早く検証し、変化に即応するためのデータに基づいた意思決定が向き合いの事業の成長にも寄与できる可能性も広がります。 その中で何を使うか、どのように評価するかはそれぞれの状況によってメリット・デメリットがあるかとは思いますが、 データを安全な状態で利用することができる環境を作っていくということはデータエンジニアとして継続的に取り組んでいくべき重要な課題だと改めて認識しました。
sealed Class とは ディープリンクの制御を sealed Class を利用してリファクタリングしてみる リファクタリング前のコード リファクタリングの内容 使用例 責務を分離することにより得られた恩恵 sealed Class により得られる恩恵 終わりに エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。私は普段はバックエンドの開発を行っていますが、数ヶ月ほど前から Flutter アプリの開発にも従事しております。エブリーではここ数年いくつかの Flutter アプリを開発/運用しています。 今回は Flutter アプリに機能追加する中で、 sealed Class を使ってリファクタリングを経て機能追加してみたので、その例を紹介します。 sealed Class とは Dart における sealed Class は そのクラス自体のインスタンスを生成できない点で abstract Class と似ています。 abstract Class と異なる点として、 Exhaustiveness checking (意訳: 網羅性チェック) により、 switch 文やパターンマッチングでサブクラスを網羅的に扱えることがあります。これにより、例えば switch 文でケースの書き漏れが存在するとコンパイルエラーとなり気づくことができます。 enum を使うことでも網羅性チェックを行えますが、列挙子それぞれにはフィールドやメソッドを持たせることはできません。一方 abstract Class ではフィールドやメソッドを持つことができますが、網羅性チェックは行えません。 sealed Class にはすべてのサブタイプは同じファイルに定義するという制約がありますが、それを受け入れることで網羅性チェックが可能となり、結果的に abstract Class と網羅性チェックそれぞれの恩恵を受けられると理解しています。 ディープリンクの制御を sealed Class を利用してリファクタリングしてみる アプリのディープリンク経由での起動のハンドリングを実装する際、URL パスごとに処理や遷移先画面が異なり、またそれぞれ特定のパスパラメータやクエリパラメータを期待するかと思います。 sealed Class の特徴を利用することで、URL パスごとに異なるパラメータをコード上に表現しながら、安全かつ見通しよくロジックを記述できると思い、実際にリファクタリングを行いました。 リファクタリング前のコード 以下はディープリンクに応じ、いくつかの条件を満たす場合に画面遷移を行うコードです。 // app.dart // リンクを受信した場合に呼ばれ、それを処理する関数 void openDeepLink(Uri uri) { final hostName = uri.host; if (hostName.isEmpty) return ; switch (hostName) { case 'news' : if (uri.pathSegments.length == 1) { final newsId = int .parse(uri.pathSegments[0]); // newsId を使って特定のお知らせの画面へ遷移 } break ; case 'carts' : // カート画面へ遷移 break ; default : // 未知のリンク break ; } } このコードには以下の問題があります: 責務が混在している URL に応じたバリデーション、値の取り出し、画面遷移の処理がすべて app.dart というアプリ全体の設定を書くファイル内の openDeepLink 関数に含まれています。単体テストは困難で、色々なパターンの URL を実際に踏むなどし動作確認をするしかなく、非効率です。 URL 構造の表現の欠落 それぞれの URL パスがどういう意味のパラメータの存在を期待しているか(いないか)が、コード上に明確に現れていません。 'news' のケースでは myapp://news/:id といった形式を期待しますが、 uri.pathSegments.length といったプリミティブな要素を扱うため、仕様と処理の一致が読み取りにくくなっています。 これらの課題を解決するため、リファクタリングをしてみます。 リファクタリングの内容 DeepLink という sealed Class を定義し、URL が特定の条件を満たしているかを確認し、値を取り出せるようにする責務を対象の関数から分離しました。結果下記のようになりました: // deep_link.dart sealed class DeepLink { final Uri uri; DeepLink._( this .uri); factory DeepLink.fromUri(Uri uri) { switch (uri.host) { case 'carts' : return CartDetailDeepLink.fromUri(uri); case 'news' : return NewsDetailDeepLink.fromUri(uri); } throw FormatException( 'Unknown URI pattern. URI: $uri' ); } } class NewsDetailDeepLink extends DeepLink { final int newsId; NewsDetailDeepLink(Uri uri, this .newsId) : super ._(uri); factory NewsDetailDeepLink.fromUri(Uri uri) { if (uri.pathSegments.length != 1) { throw FormatException( 'Invalid NewsDetailDeepLink URI: $uri' ); } return NewsDetailDeepLink(uri, int .parse(uri.pathSegments[0])); } } class CartDetailDeepLink extends DeepLink { CartDetailDeepLink(Uri uri) : super ._(uri); factory CartDetailDeepLink.fromUri(Uri uri) { if (uri.pathSegments.isNotEmpty) { throw FormatException( 'Invalid CartDetailDeepLink URI: $uri' ); } return CartDetailDeepLink(uri); } } 使用例 リファクタリング元の関数は以下のように書き換えました。 // app.dart void openDeepLink(Uri uri) { final DeepLink deepLink; try { deepLink = DeepLink.fromUri(uri); } catch (e) { // エラーハンドリング } switch (deepLink) { case NewsDetailDeepLink(: final newsId): // `:final newsId` の記述でオブジェクトのプロパティをローカル変数として利用 // newsId を使って特定のお知らせの画面へ遷移 break ; case CartDetailDeepLink: // カート画面へ遷移 break ; } } 責務を分離することにより得られた恩恵 URL 構造のコードでの表現 どの URL パスがどういう意味のパラメータの存在を期待しているか(いないか)、をコード上に明確に表現できるようになりました。 見通しの良さ、テスト可能性の向上 URL の解析と画面遷移のロジックを分離することで、コードの見通しが良くなりました。URL の解析ロジックを独立させたことで、以下のように単体テストを簡単に記述できます。なおテスト対象が単純になると、テストコードの記述を AI 任せにするのも楽になります。 group( 'DeepLink.fromUri' , () { test( 'should return NewsDetailDeepLink for valid news URI' , () { final uri = Uri.parse( 'example://news/123' ); final deepLink = DeepLink.fromUri(uri); expect(deepLink, isA<NewsDetailDeepLink>()); final newsDeepLink = deepLink as NewsDetailDeepLink; expect(newsDeepLink.newsId, 123); }); }); sealed Class により得られる恩恵 今回責務の分離に加え、 sealed Class の使用による恩恵も受けられるようになりました。 網羅性チェック switch 文で default ケースを記述する必要がなくなりました。かつ、case の漏れが生じた場合にはコンパイル時にエラーとなるため、漏れを防げます。 コードの一貫性 サブクラスを同じファイル内に記述する制約が生まれることで、関連するコードが一箇所にまとまり、メンテナンス性が向上します。 終わりに sealed Class を使って、実際にコードをリファクタリングした例をご紹介しました。 まだまだ未熟な Flutter エンジニアですが、Flutter / Dart の開発業務の中で知見を深めまたご紹介できればと思います。お読みいただきありがとうございました。
はじめに こんにちは!トモニテで開発を行っている吉田です。 今回は Serverless Framework で作成した Lambda 関数を lambroll に移行しようとしたことについて書きます! 移行検討の背景 昨年、Serverless Framework の v4 がリリースされました。 v4 からはライセンス形態が変更されて、収益の閾値を満たす、あるいは超える組織では有料でサブスクリプションを購入する必要があります。また v3 は 2024 年までのサポートで、クリティカルなセキュリティ問題やバグにしか対応しません。 他にもランタイムのアップデートにも対応していないため、今は大丈夫でもいずれ AWS Lambda のランタイムサポートが切れるため、v3 から v4 へのバージョンアップが必要になります。 www.serverless.com そこで Serverless Framework v3 を v4 に上げる以外の方法について検討しました。 Serverless Framework からの移行先候補について 1. Pulumi 概要 : Go や TypeScript などの汎用プログラミング言語で利用できるマルチクラウド対応の IaC ツール メリット: Go, TypeScript, Python などで記述できる ユニットテストが書ける 関連リソースもデプロイ可能 デメリット: 料金が発生する可能性あり。規模によっては無料枠に収まるが、料金体系が変更された場合には今回と同様に移行先の検討が必要になる 2. AWS CDK (AWS Cloud Development Kit) 概要 : Pulumi と同様に汎用プログラミング言語で利用できる AWS 向けの IaC ツール。AWS CloudFormation を通じてインフラストラクチャのリソースをデプロイ。Serverless Framework でデプロイしたスタックから移行できる cdk migrate というツールはあるが、懸念事項あり。 メリット: Go, TypeScript, Python などで記述できる 関連リソースもデプロイ可能 無償で利用可能 デメリット: cdk migrate で移行した場合、構造が最適化されない可能性がある。 上記のこともあり移行工数多め docs.aws.amazon.com 3. AWS SAM (AWS Serverless Application Model) 概要 : Serverless Framework と同様に yaml ベースでインフラ定義を記述して、CLI から操作するツール。AWS CloudFormation を通じてインフラストラクチャのリソースをデプロイする。 メリット: Serverless Framework と同じく yaml ベースでの定義設定 関連リソースもデプロイ可能 無償で利用可能 デメリット: Serverless Framework よりもプラグインは少なくなる 4. fujiwara/lambroll 概要 : Lambda 関数をデプロイすることに特化したツール。 メリット: tfstate を参照して連携できる。 CD の用意あり 無償で利用可能 社内での利用実績がある。 デメリット: 関連リソースは Terraform で管理する必要がある 5. Terraform + AWS CLI 概要: 既存の Terraform でのインフラ管理に便乗するプレーンな手段 メリット: インフラ管理ツールを 1 つにまとめられる(弊社的都合) 学習コストは低い 無償で利用可能 デメリット: 自動デプロイなどの CD に関してフローを整理する必要がある 学習コストや運用実績などを鑑み今回は fujiwara/lambroll(以下 lambroll)で移行が可能なのか調査を行いました。 (もし移行できたとしても CloudFormation で作成したものを途中で別のツールへ移行して不具合が起きないのか等と懸念事項はありますが今回はその点は考慮せず一旦ライトに実験しました) Serverless Framework と lambroll の違い 移行にあたり両者の違いは理解しておく必要があります。 Serverless Framework の特徴としては以下が挙げられます。 Lambda をメインとするインフラリソースをデプロイする デプロイは CloudFormation を使用して行われる 一方 lambroll は Lambda 関数のデプロイのみを行い Lambda 関数に関連したリソースをデプロイすることはできません。 lambroll does not, Manage resources related to the Lambda function. For example, IAM Role, function triggers, API Gateway, and etc. Only the function URLs can be managed by lambroll if you want. Build native binaries or extensions for Linux (AWS Lambda running environment). ( https://github.com/fujiwara/lambroll より) 移行できるか試してみる config ファイルを lambroll で作成しそれ以外は変更せずデプロイ 既存関数のダウンロード(LATEST で動作している zip をダウンロードできる) $ lambroll init --download ダウンロードすると Lambda 関数の定義となる function.json が作成されるので設定値を調整 デプロイ(ログを詳細に確認したかったので log-level オプションを設定) $ lambroll deploy --envfile=<環境別の設定ファイル> --log-level=debug デプロイ時のログを確認すると tag の設定に失敗していました。 2025 / 02 / 20 17:14:29 [ info ] lambroll v1. 1 . 3 2025 / 02 / 20 17:14:29 [ debug ] { " Src " : " . " , " Publish " :true, " AliasName " : " current " , " AliasToLatest " :false, " DryRun " :false, " SkipArchive " :false, " KeepVersions " :0, " Ignore " : "" , " FunctionURL " : "" , " SkipFunction " :false, " ExcludeFile " : " .Lambdaignore " , " KeepSymlink " :false } 2025 / 02 / 20 17:14:29 [ info ] starting deploy function < 関数名 > 2025 / 02 / 20 17:14:30 [ info ] creating zip archive from . (一部省略) 2025 / 02 / 20 17:14:30 [ info ] updating function configuration ... 2025 / 02 / 20 17:14:30 [ info ] State:Active LastUpdateStatus:Successful 2025 / 02 / 20 17:14:31 [ info ] updating function configuration accepted. waiting for LastUpdateStatus to be successful. 2025 / 02 / 20 17:14:31 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 02 / 20 17:14:31 [ info ] waiting for LastUpdateStatus Successful 2025 / 02 / 20 17:14:32 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 02 / 20 17:14:32 [ info ] waiting for LastUpdateStatus Successful 2025 / 02 / 20 17:14:34 [ info ] State:Active LastUpdateStatus:Successful 2025 / 02 / 20 17:14:34 [ info ] updating function configuration successfully 2025 / 02 / 20 17:14:34 [ debug ] 7 tags found 2025 / 02 / 20 17:14:34 [ debug ] remove tag cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag aws:cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag aws:cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ debug ] remove tag aws:cloudformation: < tag名 > 2025 / 02 / 20 17:14:34 [ info ] removing 6 tags 2025 / 02 / 20 17:14:34 [ error ] FAILED. failed to untag resource: operation error Lambda: UntagResource, https response error StatusCode: 400 , RequestID: 6798028d-f699-4508-b000-0ba7b5eab222, InvalidParameterValueException: One or more of your tags had a problem, please ensure none of your tag names start with " aws: " , none are empty, and there are no duplicate tags ただ config の更新は成功していそうです。 2025 / 02 / 20 17:14:34 [ info ] updating function configuration successfully 実際にコンソールで確認すると、S3 に zip ファイルはアップロードされ、Lambda 関数が動作することも確認できました。 ただ、デプロイされたファイルのサイズは Serverless でアップロードしたものより大きくなってしまっていたので、この辺りはちゃんと見直す必要がありそうです。 上記で config ファイルがデプロイできるのが確認できたので試しにランタイムの更新ができるか試したところこちらも問題なくできました。 Lambda 関数の処理内容を変更 config は設定できたので処理内容を変更してみました。ログは先ほどと同様にタグの設定でエラーが出ました。 2025 / 02 / 20 18:04:02 [ error ] FAILED. failed to tag resource: operation error Lambda: TagResource, https response error StatusCode: 400 , RequestID: 7e16baf5-4b77-4248-a3c9-5f3d75d13fa0, InvalidParameterValueException: One or more of your tags had a problem, please ensure none of your tag names start with " aws: " , none are empty, and there are no duplicate tags コンソールで確認したみたところ確かに aws: で始まるタグがありました。タグに関わらず設定値は基本的に既存リソースの設定をダウンロードしたものだったので特に変更はしていませんでしたが aws: で始まるのがダメならそうじゃないようにすればいいかタグ名を変更した上デプロイしたりタグを設定せずにデプロイしてみましたが、変わらずタグ操作で失敗しデプロイできず...(すでに設定された aws:~ のタグを外すこともできませんでした) 一応 AWS CLI で untag-resource コマンドを実行してみましたがこちらでもタグの変更はできませんでした。 $ aws lambda untag-resource \ --resource arn:aws:Lambda: < region > : < アカウントID > :function: < 関数名 > \ --tag-keys aws:cloudformation:stack-name \ --tag-keys aws:cloudformation:stack-id \ --tag-keys aws:cloudformation:logical-id An error occurred ( InvalidParameterValueException ) when calling the UntagResource operation: One or more of your tags had a problem, please ensure none of your tag names start with " aws: " , none are empty, and there are no duplicate tags リソースの serverless.yml を見ても tag を明示的に指定しているところがなかったので調べたところ、 CloudFormation ではプレフィックス aws: が付いた以下のようなスタックレベルのタグを自動的に作成するとのことでした。 aws:cloudformation:logical-id aws:cloudformation:stack-id aws:cloudformation:stack-name https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html さらに以下の記述もありました。 aws: プレフィックスは AWS 用に限定されています。このプレフィックスでは、大文字と小文字は区別されません。Key または Value プロパティでこのプレフィックスを使用すると、タグを更新または削除することはできません。 タグのエラーが出る原因が分かりました。 今回の調査では、CloudFormation によって自動的に付与された aws: プレフィックスのタグが原因で、lambroll からのタグ操作が失敗し、Serverless Framework で作成した Lambda 関数を lambroll に移行することは難しいという結論に至りました。 終わりに Serverless Framework で作成した Lambda 関数を lambroll に移行することはできませんでした。 もし Serverless Framework から lambroll へ乗り換える場合には以下の手順で作業を行う必要がありそうです。 既存関数のダウンロード( $ lambroll init --download ) 既存の関数と同等の関数ができるように function.json の設定値を調整 Serverless で作成したリソースを削除 lambroll で Lambda 関数デプロイ lambroll は既存の Lambda 関数をダウンロードすることができるので、移行する時のハードルはそこまで高くないのかなと感じました。また lambroll では関連リソースが作成されないため、これについては terraform に移行するなど別で検討が必要になります。 最後まで読んでいただきありがとうございました! 2025/5/15 追記 なんとこのブログを lambroll の作者である fujiwara さんに読んでいただけました! なるほど、aws: prefixが付いたタグを無視するようにしたら解決する? / “Serverless Framework で作成した Lambda 関数を lambroll に移行できるのか調査しました - every Tech Blog” https://t.co/VdaupqClp6 — fujiwara (@fujiwara) 2025年4月15日 そして 4/25 に更新された リリースノート の中に ignore AWS managed tags for tag/untag operation. という変更が!! 早速最新のバージョン v1.3.0 に上げてブログ内の操作を再トライしてみました! ※ログは一部省略しています $ lambroll deploy --envfile = .env.dev --log-level = debug 2025 / 05 / 14 21:09:41 [ info ] lambroll v1. 3 . 0 ... 2025 / 05 / 14 21:09:44 [ info ] updating function configuration ... 2025 / 05 / 14 21:09:44 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:45 [ info ] updating function configuration accepted. waiting for LastUpdateStatus to be successful. 2025 / 05 / 14 21:09:45 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:45 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:46 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:46 [ info ] updating function configuration successfully 2025 / 05 / 14 21:09:46 [ debug ] 4 tags found 2025 / 05 / 14 21:09:46 [ info ] ignoring AWS managed tag aws:cloudformation:stack-id 2025 / 05 / 14 21:09:46 [ info ] updating function code ... 2025 / 05 / 14 21:09:46 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:47 [ info ] updating function code accepted. waiting for LastUpdateStatus to be successful. 2025 / 05 / 14 21:09:47 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:47 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:48 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:48 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:50 [ info ] State:Active LastUpdateStatus:InProgress 2025 / 05 / 14 21:09:50 [ info ] waiting for LastUpdateStatus Successful 2025 / 05 / 14 21:09:55 [ info ] State:Active LastUpdateStatus:Successful 2025 / 05 / 14 21:09:55 [ info ] updating function code successfully 2025 / 05 / 14 21:09:55 [ info ] deployed version 30 2025 / 05 / 14 21:09:55 [ info ] updating alias set current to version 30 2025 / 05 / 14 21:09:55 [ info ] alias current is not found. creating alias 2025 / 05 / 14 21:09:55 [ info ] alias updated エラーは出ず無事にアップロードが完了していることをコンソールで確認できました。 fujiwara さんにこのブログが届くとは思っていなかったので、X(旧 Twitter)にポストしていただけただけでも驚きでしたが、ブログの内容を反映したものもアップデートに含んでいただけたことにはさらに驚きました。本当にありがとうございます!
はじめに ヘルシカについて モジュール化の利点 アーキテクチャと関心の分離の徹底 ビルド時間の短縮 テストの容易性 ヘルシカ Android アプリ既存のモジュール化戦略 既存のモジュール化戦略の問題点 改善後のモジュール化戦略 モジュールの分類 既存モジュールの命名変更 feature モジュールの新設 まとめ はじめに Android 開発エンジニアを担当している岡田です。 サービスの成長に伴い、コードの肥大化・複雑化は避けられないものだと思います。 しかしながらサービスの成長角度を下げないためには、持続可能で保守性の高いコードを保つ必要があります。 今回は弊社のサービスであるヘルシカにて、上記の問題を改善すべく Android アプリのモジュール化について再検討・実装しましたのでお話しさせていただきます。 ヘルシカについて ヘルシカは健康的な生活を送るためのヘルスケアアプリです。 この機能を使うことで、食事や体重、体脂肪率を記録できます。 食事の記録ではカロリー計算により健康的な食生活を目指せます。 ヘルシカは以下のリンクからダウンロード可能です!是非、利用してみてください。 ヘルシカ -健康管理のための食事記録・体重管理アプリ every, Inc. ヘルスケア/フィットネス 無料 apps.apple.com play.google.com モジュール化の利点 モジュール化が話題になって数年が経ちましたが、ここで改めてメリットについて簡単にまとめたいと思います。 以下に自分が特に大きいと感じているメリットを 3 つ選出しました。 アーキテクチャと関心の分離の徹底 アーキテクチャに従って機能や役割ごとにモジュールを分割することで、コードの可読性・保守性が向上します。 モジュール間の依存関係や設計意図をコードレベルで表現できるため、新規参入者にアーキテクチャを共有しやすくなります。 ビルド時間の短縮 モジュールごとに独立してビルドできるため、変更があったモジュールのみを再ビルドすることで、全体のビルド時間を短縮できます。 特に大規模なプロジェクトでは、差分ビルドによる恩恵が大きくなります。 テストの容易性 モジュールごとに独立してテストできるため、ユニットテストや結合テストを効率的に実行できます。 テストカバレッジを向上させ、品質の高いアプリケーションを開発できます。 他にも「コードの再利用性が向上する」、「 チームで開発が分割しやすくなる」などの利点があります。 詳しくは是非、Google Developers の公式ドキュメントを参照ください。 developer.android.com developer.android.com 自分の中では「 アーキテクチャと関心の分離の徹底 」を一番の利点に感じています。 他の利点も重要ですが、コードの構造的な強制力はモジュール化ならではの効果であるためです。 今回はこちらを重要視してモジュール化戦略を再考しました。 ヘルシカ Android アプリ既存のモジュール化戦略 ヘルシカ Android は、既にモジュール化されたプロジェクトでした。 モジュール分割は以下の通りになっており、 各モジュールは、 modules というディレクトリ配下に作成されていました。 モジュール名 説明 app アプリケーションのエントリーポイント。機能に関するコードも保持している api ネットワーク通信や外部APIとの連携を担う debug デバッグビルド専用のツールや機能を提供する core アプリケーション全体で共通して使用される機能やユーティリティを提供する repos repository の略で、データのリポジトリパターンを実装する resource アプリケーションで使用する共通のリソースを管理する usecase アプリケーションのビジネスロジックを実装する widget 再利用可能なUIコンポーネントを提供する model アプリ内で使用する共通のモデルクラスを実装 する 既存のモジュール化戦略の問題点 既存のモジュール化戦略には以下のように、いくつか問題点がありました。 api や repos 、 usecase に関して、パターン名を命名に使用しているため、アーキテクチャを意識しづらい widget や core に関して、初見で何が格納されているのか意識しづらい core と他モジュールの区分が曖昧 各機能に関するコードが app 配下に存在しているため、依存関係が曖昧 modules ディレクトリが不要である repos に関して、略称が使用されており分かりにくい 特に 1 ~ 4 に関して、モジュール化のメリットである「アーキテクチャと関心の分離の徹底」を、十分に享受できていないような戦略になっていることがもったない点でした。 改善後のモジュール化戦略 既存の問題点を改善するためのモジュール化戦略は以下の通りです。 モジュール化戦略を再考するにあたり、 Now In Android > Modularization learning journey を参考にしました。 モジュールの分類 大きな変更点として modules ディレクトリを削除し、すべてのモジュールを core モジュールと feature モジュールの 2種類に分類 しました。 ヘルシカでは現時点、機能に関するコードは app モジュール配下に格納されているため、既存のモジュールはすべて core モジュール配下に移動しました。 モジュール名 説明 core モジュール間で共有する必要があるコード feature 単一の責任を処理するようにスコープ設定されたコード この 2 つのモジュールについては、依存関係が重要です。 core モジュールは他のモジュールから依存されても良い feature モジュール は app モジュール以外からの依存を許さない 依存関係を強制することで、モジュール化のメリットである「アーキテクチャと関心の分離の徹底」を享受できます。 既存モジュールの命名変更 以下の通りにモジュールの命名を変更しました。 変更前 変更後 api network repos data usecase domain core common widget ui api 、 repos 、 usecase はパターン名でなく、具体的な役割を示す名前に変更しました core は共通機能を含むことをより明確にするために common に変更しました widget は UI に関連する要素を含むことを示すために ui に変更しました 命名変更することで、アーキテクチャについて意識しやすくなりました。 また Now In Android とモジュールの命名を似せたため、新規参入者に伝わりやすくなったと思います。 feature モジュールの新設 現時点では app 配下にすべて格納されている状態は、関心の分離の観点から好ましくないです。 従って、単一の機能を持つ feature モジュールを新設しました。 今後は機能ごとに feature モジュール配下へモジュールを作成し、実装を進めていきます。 まとめ ヘルシカ Android アプリのモジュール化戦略を再考し、以下の改善を実施しました。 モジュールの分類 既存モジュールの命名変更 feature モジュールの新設 これらの改善により、アーキテクチャと関心の分離の徹底を図りました。 現在ヘルシカはこのモジュール化戦略に従い、リファクタリング作業の真っ只中です。 今後も健全かつ人間にとって優しいコードを目指していきたいと思います!
目次 はじめに 前提 挑戦WEEK とは 実施にあたって気をつけていること 施策の流れ 実際に行われたもの 得られた効果 今見えている問題 まとめ 最後に はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 4月で社会人 9 年目に突入し、もう若手とは言えない年齢になっており驚きを隠せません。 今回は弊社での組織活性施策の一環として行っている 挑戦WEEK の効果の振り返りを行います。 社内外からこの取り組みに伴う効果を聞かれることが多くなってきたので、実施した内容や効果を振り返ることで、今後の施策に活かせればと思います。 挑戦WEEK 自体に関する情報は過去に紹介しているのでぜひご覧ください。 tech.every.tv 運営に関しても過去の記事で紹介されています。 tech.every.tv 前提 エブリーでは「デリッシュキッチン」、「リテールハブ」、「トモニテ」、「TIMELINE」の複数のプロダクトを開発しています。 その中で、開発部は各プロダクトの開発を行うチームが存在し、各チームはそれぞれ異なる技術スタックや開発環境を持っています。 例えば私の所属する「トモニテ開発部」ではサーバーサイドとフロントエンドを一括りにしたチームと、クライアントアプリ専門のチームが存在します。 また、データや AI などより専門性が高い領域に対しては横断組織が存在し、各プロダクトの開発を支援しています。 この組織体制上だと他のチームでの出来事や技術的な挑戦を知る機会が少なく、各チームのメンバー同士の交流も少ないため、ナレッジ共有やコミュニケーションの活性化が課題となっています。 そのため行っている施策の一つに 挑戦WEEK があります。 挑戦WEEK とは 挑戦WEEK とは、普段の業務から離れ、1 週間かけて技術的な挑戦に取り組むイベントです。 技術的な挑戦を通して、エンジニア個人の成長と組織の活性化を促進することを目的としています。 通常業務で抱えている課題の解決(例:パフォーマンス改善)、新しい技術の検証(例:AI 技術の導入検証)、開発環境の改善(例:CI/CD パイプラインの構築)など、普段は時間を確保しにくいテーマに集中して取り組むことができます。 直近だと生成 AI 技術を活用した機能やツールの開発が行われました。 また、他チームのメンバーの交流を深める機会にもなっています。 実施にあたって気をつけていること まず「1 週間は事業部の施策や MTG には参加せず、集中して 挑戦WEEK に取り組む」ためにするべきは、各事業部の皆様にも協力していただく必要があります。 1 週間施策を止めることは、その期間プロダクト開発が止まり売上やサービスに影響が出てしまうため、多大な影響を与えることになります。 そのため実施する内容の精査は慎重に行っており、1 週間できる最大限の成果や知見が得られるような内容に調整しています。 これまでで 5 回実施してきましたが、ただ新しい技術を試したいだけではなく、将来組織やプロダクトに貢献できるようなものを選定しています。 もちろん、完璧に作り切るものを求めるのではなく、1 週間での成果物を作成することを目的としています。 施策の流れ 挑戦WEEK ではメンバーからの提案(プロポーザル)を募集し、選定されたものをチームで取り組む形で実施しています。 挑戦内容の募集 プロポーザル選定 投票 チーム分け 挑戦WEEK の実施(最終日成果発表会) 実施後アンケート 以前は各リーダーやマネージャーから提案してもらう形でしたが、参加するメンバー自ら意見を出すことで普段マネジメント層から見えてこない技術的な課題や興味を持っているテーマを知ることができ、より多くのメンバーの参加の意識が高まるようになりました。 実際に行われたもの 挑戦WEEK の実施内容は、毎回異なりますが、過去の実施内容を振り返ると大きく分けて以下のようなテーマがありました。 共通基盤の技術検討 新しい技術の検証 パフォーマンスやセキュリティの改善 AI やビッグデータ活用 異なるプロダクトのコラボレーション 普段であればコストを意識しすぎて出来なかったり、今のプロダクトのロードマップから離れてしまってタイミングが見つからなかったりするテーマに挑戦することができるため、メンバーのモチベーションも高く、普段の業務では手が出しづらいテーマを行ってきました。 実際に実施されたものに関しては以下で紹介されています。 tech.every.tv tech.every.tv tech.every.tv tech.every.tv 得られた効果 技術的な挑戦を通じて、メンバー同士の技術的な知見の共有が進み、普段の業務においても新しい技術やアプローチを取り入れる機会や姿勢が芽生えてきています。事業部側でも 挑戦WEEK でやってもらいたい技術検証を依頼されることもあり、 挑戦WEEK の成果をプロダクトに活かす機会が増えてきました。 実際に 挑戦WEEK で何度か生成 AI に対する取り組みが行われておりその中で得られた知見を基に、プロダクトに組み込むことができるようになってきました。 最初の頃は OpenAI を通して生成 AI を使ってみるところからで右も左もわからない状態ではありましたが、 挑戦WEEK を 2~3 回重ねるごとに 1 機能として実験的に作成するところまで到達できるようになってきました。 この知見を基に実際に動いているプロダクトで正式ロードマップの中で実装されるようにり、メインのプロダクトである「デリッシュキッチン」に生成 AI を活用した「デリッシュ AI」という機能が実装され、ユーザーのレシピ検索をサポートしています。 アーキテクチャに関しては以下の記事で紹介されています。 tech.every.tv また弊社の VP of Product からの AI/LLM によって、toC 向けサービスの在り方、プロダクトの作り方に関する紹介もありますので、ぜひご覧ください。 AI/LLMでtoC向けサービスはどう変わるのか?『DELISH KITCHEN』は、「レシピ動画アプリ」から「AI料理アシスタント」へ また開発組織のメンバー同士の交流は、初回実施した 2023 年 3 月と比較するとコミュニケーションが活発になってきており、普段の業務では関わることのないメンバー同士での交流が生まれています。一度同じチームで 1 週間過ごして開発を行うことで、戦友のような一体感が生まれ、気軽に相談しやすくなったり、他チームのメンバーと話す機会が増えています。 今見えている問題 挑戦WEEK の実施を通じて、メンバー同士の交流やナレッジ共有が進んできていますが、まだまだ問題も残っています。 出してもらったプロポーザルが精査や投票の中で、技術的な実現可能性やスケジュール、リソースなどの制約により実施できないものが多く、メンバーの意見を十分に反映しきれていない部分がありました。 実施後のアンケートから選考プロセスの見直しや、結果の伝え方を工夫することで、メンバー全員が納得できるようなプロポーザルの選定を行う必要があります。 また 挑戦WEEK で生まれたものがすべてそのままプロダクトに活かせるわけではありません。実際にプロダクトに組み込むためには、現在のロードマップや仕様要件を満たす必要があります。 作成したものがそのまま使えなくても、挑戦の過程で得られた知見はプロダクトに活かすことができます。どんな挑戦をしてどんな知見を得られたかを社内外に発信することで、1 週間の事業部の施策を止めてまで行ったことの意義を感じてもらえるようにしていきたいです。 まとめ 挑戦WEEK は、エンジニア個人の成長と組織の活性化を促進するための施策として、技術的な挑戦に取り組むイベントです。 普段の業務から離れ、集中して取り組むことで、メンバー同士の交流やナレッジ共有が進み、エンジニアリング文化の醸成に寄与しています。 また、 挑戦WEEK で得られた知見や技術をプロダクトに活かす機会も増えてきており、組織全体の成長にもつながっています。 大事なこととして各事業部側の皆さんに協力の上で成り立っています、改めて感謝申し上げます。 今後も 挑戦WEEK を通じて、エンジニアリング文化の醸成や組織の活性化を促進していきたいと思います。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
はじめに こんにちは、デリッシュキッチン開発部でソフトウェアエンジニアをしている新谷です。 エブリーの開発部では「挑戦week」という1週間の期間限定チャレンジを定期的に開催しています。これは日常業務から離れて、新しい技術やアイデアに挑戦する取り組みです。 今回は、この挑戦week期間中にデリッシュキッチンの検索基盤をElasticsearchからOpenSearchへ移行する挑戦を行いましたので、その内容を紹介します。 ※ 挑戦weekの詳細については過去の記事で紹介していますので、興味のある方は以下をご覧ください。 tech.every.tv 背景:なぜElasticsearchの移行が必要なのか デリッシュキッチンでは、レシピ検索とサジェスト機能のために長らくElasticsearch 5.5をECS上で運用してきましたが、以下のような課題がありました。 バージョンの古さ :Elasticsearch 5.5は既に数年前のバージョンであり、セキュリティ面や機能面で最新版に劣っています 検索周りの施策展開の困難さ :古いバージョンであるため、新しい検索機能の実装が難しくなっていました 運用負荷 :ECS上での自前運用による保守管理コストがかかっていました これらの課題を解決するために、ElasticsearchからAWSが主導開発しているOpenSearchへの移行を検討することになりました。 ElasticsearchとOpenSearchとは OpenSearchはElasticsearchからフォークする形で誕生したオープンソースの検索エンジンです。元々Elasticsearchは完全なオープンソースでしたが、Elastic社はバージョン7.11以降からライセンスをElastic License 2.0に変更しました。これに対しAWSは「完全にオープンソース」の状態を維持するためにOpenSearchというフォークプロジェクトを立ち上げました。 現在、それぞれの開発は並行して進められていますが、AWS上のマネージドサービスとしてはOpenSearchが優先的にサポートされています。 項目 Elasticsearch 7.11以降 OpenSearch ライセンス Elastic License 2.0 / SSPL Apache License 2.0 商用利用 制限あり(SaaS提供には制限) 完全に自由 ソースコードの変更と再配布 許可されるが制限あり 完全に自由 SaaS提供 制限あり 制限なし 開発主体 Elastic社 AWS主導のコミュニティ開発 挑戦weekでの取り組み 「挑戦week」というわずか1週間のプロジェクト期間と3人という少人数チームでの取り組みだったため、移行にあたっては現実的な範囲で以下のようなステップを設定しました。 ローカル環境でのOpenSearchの導入 ローカル環境での動作検証 (将来的に実施)AWS インフラ構築 (将来的に実施)CI 整備 当初は4つのステップすべてに挑戦する予定でしたが、実際に取り組んでみると想定以上に複雑な課題が見つかり、一週間では最初の2ステップである検証部分しか実施できませんでした。それでも、この限られた期間で多くの貴重な知見を得ることができました。 実施内容:ローカル環境での検証 ローカル環境でElasticsearchからOpenSearchへ切り替えるために、以下の作業を行いました。 Docker環境の整備 OpenSearchのDockerコンテナを使用するように設定を変更するため、Dockerfileの修正を行いました。具体的には、 elasticsearch/elasticsearch から opensearchproject/opensearch へDockerイメージを変更しています。また、docker-compose.ymlも対応する形で修正し、ローカルでOpenSearchを立ち上げられるようにしました。 クライアントライブラリの検証 デリッシュキッチンではGoのクライアントライブラリとして gopkg.in/olivere/elastic.v5 を使用していましたが、OpenSearchへの移行に伴い、 opensearch-project/opensearch-go/v4 への切り替えが必要になりました。 検証の結果、以下のような課題が見つかりました。 数十メソッドの書き換えが必要 新しいライブラリは比較的薄いため、リクエストボディを自前で組み立てる必要がある 当初の想定を大きく上回る作業量が必要であることが判明しました。一週間の限られた時間では全容を把握するだけでも大変でした。 データ投入スクリプトの整備 OpenSearchにテストデータを投入するために、スクリプト化してmakeコマンドから実行できるように改善しました。これにより、繰り返しテストを行いやすくなりました。 主な技術的課題 検証過程で明らかになった主な変更点と課題について解説します。 1. _typeパラメーターの廃止 Elasticsearch 5.5では _type パラメータを使用して、1つのインデックス内で異なる種類のドキュメントを区別していました。しかし、OpenSearch 2.19では _type は完全に廃止され、1インデックス1種類のドキュメントという設計に変更されています。 当初のマッピング定義では、ingredients、recipes、tagsの3つのドキュメントが1つのインデックスで定義されており、投入時に _type=recipes を指定することで対応していました。移行するには、「recipes」だけの定義に変更し、 _type 指定なしでも動作するように修正する必要があります。 これはデータ構造の見直しを意味し、単純な置き換えでは解決できない課題です。 2. インデックスマッピングの厳格化 OpenSearch 2.19ではマッピングの制約が強化されています。特にシノニム(同義語)の設定ファイルに関して、Elasticsearch 5.5では許容されていた一部不適切な定義がOpenSearchではエラーとなりました。 具体的には、以下のようなシノニム定義ファイルの問題が見つかりました。 形式が不正な同義語の定義 重複する定義 解析できない文字が含まれる定義 一週間で全て修正するには時間が足りないため、一時的な対応としてシノニム部分に "lenient": true 定義を追加し、問題のある行があっても無視するよう対応しました。本来は、シノニム定義を見直し、問題のある行を修正する必要があります。 3. クエリ仕様の変更 OpenSearch 2.19ではboolクエリのパフォーマンスが向上しているというメリットがありますが、以下のような変更点がありました。 _all フィールドの削除(元々falseで使用していなかったため、定義自体を削除) allow_leading_wildcard のデフォルト無効化 default_field の明示的指定必須化 その他、細かな仕様変更 これらの変更により、既存の検索クエリの多くを見直す必要があり、検索結果への影響を一つひとつ検証する必要があります。 4. xpackプラグイン xpackプラグインについては、OpenSearchではサポートされていないため、代替機能の検討が必要です。一週間という限られた時間内では、詳細な検討までは至りませんでした。 検索品質の検証結果 ローカル環境での検証後、実際の検索品質を評価するために比較テストを行いました。直近のアクセスログから1,000件の検索ワードをサンプリングし、ElasticsearchとOpenSearchの両方で検索して結果を比較しました。以下が比較した結果の一部です。 結果としては、どちらも一定の妥当性のある検索結果が得られましたが、複数の問題点が発覚しました。 1. 検索結果の品質低下 特定の検索ワードでOpenSearchのほうが劣っているケースが見つかりました。例えば、「麻薬たまご」という検索ワードでは、以下のような違いがありました。 Elasticsearch: 「麻薬たまご」が一つの検索語として扱われ、「麻薬卵」レシピがヒット OpenSearch: 「麻」「薬」「た」「ま」「ご」のように分かれてしまい、「麻薬卵」レシピがヒットしない このような違いは、トークナイザーやアナライザーの設定の違いによるものと考えられます。 2. その他の課題 検証結果から、ElasticsearchとOpenSearchにおいて明確に差異はありますが、なぜ差異がありどのようなパターンで差異が出るのかは明確になっていません。 「麻薬卵」は一例であり、その他の差異については検討が必要です。 今後の検討事項 挑戦weekの取り組みで明らかになった課題を踏まえ、今後取り組むべき検討事項は以下の通りです。 1. シノニム・辞書整備 現状の問題のある定義を洗い出して、Elasticsearchのシノニム定義をOpenSearchのシノニム定義に変更 日本語形態素解析の設定見直し、Elasticsearchと同等の設定にする 2. 段階的移行計画 ユーザー体験の急激な変化を避けるため、段階的な移行を立てる 一度にOpenSearchに移行するのではなく、ElasticsearchとOpenSearchを共存させ、カナリアリリースを行う等の方法を検討 3. 検索品質の評価方法の検討 実践投入に向け、0件ヒット率等の検索品質の評価方法を検討する まとめ 挑戦week前は、ElasticsearchとOpenSearchの知識がほぼない状態でスタートしましたが、シノニムの設定方法からそれぞれの仕様差分まで一週間で学び、良い経験になりました。 実際に取り組んでみて、単なるバージョンアップではなく、アーキテクチャの見直しを含む大規模なプロジェクトになることが分かりました。 短期間の検証ではありましたが、今後も機会を見つけてさらなる検証と課題解決を進め、最終的には検索機能の品質を向上させる形での移行を目指したいと思います。