TECH PLAY

株式会社エブリー

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

385

はじめに Pinia Colada とは 非同期処理の課題 1. 冗長なコード 2. 状態管理の複雑さ 3. 効率的なデータフェッチング 非同期処理における様々なアプローチ 1. Vue Promised 2. swrv 3. TanStack Query (Vue Query) Pinia Colada の優位性 Pinia Colada の基本的な使い方 セットアップ 基本的なデータ取得の例 ポイント解説 まとめ はじめに この記事は every Tech Blog Advent Calendar 2024 の2日目の記事です。 はじめまして、エブリーの羽馬( @naoki_haba )です。 Vue.js アプリケーションを開発していて、次のような課題に直面したことはありませんか? API 通信のたびに似たようなコードを書くのが面倒 例:ユーザー情報の取得や商品リストの取得など、毎回似た処理を書く必要がある。 ローディングやエラー処理を丁寧に書こうとするとコードが膨れ上がる 複数の API コールやキャッシュを効率よく管理したい そんな課題を解決するために登場したのが Pinia Colada です。 この記事では、Pinia Colada の類似機能を持つ他のライブラリと比較した上で、Pinia Colada の特徴や導入方法、基本的な使い方を紹介します。 本記事は以下の登壇資料を元に執筆しています。こちらもぜひご覧ください: speakerdeck.com 記事内で紹介するサンプルコードは、StackBlitz で試せるようにしています Pinia Colada とは Pinia Colada は、Vue.js アプリケーションにおける 非同期処理 や データ管理 を効率化するためのライブラリです。 次のような機能を提供します: API リクエストの処理、キャッシュ管理、エラー処理を自動化 冗長なボイラープレートコードを削減 Vueのリアクティブなデータ管理と自然に統合 重複リクエストの防止やキャッシュ戦略の実装 DevTools対応 : Vue DevToolsとの連携により、デバッグと状態の検査が容易になります TypeScriptサポート : 優れたTypeScriptサポートにより、型安全性と開発体験を向上させます Pinia Colada は、Vue エコシステムに最適化された軽量なライブラリであり、人気の高い TanStack Query から一部 API を取り入れています。そのため、TanStack Query に馴染みがある方には特に使いやすい設計です。 また、公式サイトには TanStack Query からの移行ガイド が用意されているため、既存プロジェクトでもスムーズに導入できます。 pinia-colada.esm.dev 非同期処理の課題 非同期処理はフロントエンド開発で避けられない重要な要素ですが、アプリケーションが大規模化するにつれ、次のような課題が生じることがあります。 1. 冗長なコード API通信のたびに、ローディング状態やエラーハンドリングを記述する必要があります。以下のようなコードを何度も書いた経験はありませんか? サンプルコード <script setup lang= "ts" > import { ref, onMounted } from 'vue' const isLoading = ref( false ) const error = ref< Error | null >( null ) const data = ref< any >( null ) async function fetchData () { isLoading.value = true try { const response = await fetch ( 'https://jsonplaceholder.typicode.com/users/1' ) if (!response.ok) throw new Error ( 'データの取得に失敗しました' ) data.value = await response.json() } catch (e) { error.value = e as Error } finally { isLoading.value = false } } </script> このようなコードを何度も書くのは、時間もかかり、ミスの温床になります。 2. 状態管理の複雑さ 非同期処理には、次のような状態を適切に管理する必要があります: ローディング中かどうか エラーの内容 データが最新かどうか キャッシュの有効期限や再取得のタイミング これらをすべて手動で管理すると、コードが複雑化しやすくなります。 3. 効率的なデータフェッチング 効率的に非同期処理を扱うには、以下が重要です: 重複リクエストの防止 :同じデータを何度も取得しない仕組み キャッシュの活用 :既存データを再利用して無駄なリクエストを削減 自動再取得 :データの期限切れやネットワークの復帰時に再フェッチ これらを実現するのは容易ではありません。 非同期処理における様々なアプローチ Vue.js には、非同期処理を扱うための便利なライブラリがいくつも存在します。それぞれの特徴を簡単に紹介します。 1. Vue Promised Vue Promised は、Vue コンポーネント内で非同期状態を簡単に管理できるライブラリです。テンプレートでローディング中やエラー、結果の表示を切り替えることができます。 サンプルコード <script setup lang= "ts" > import { ref } from 'vue' import { Promised } from 'vue-promised' interface UserData { id : number name : string email : string } const userPromise = ref< Promise < UserData >>( fetch ( 'https://jsonplaceholder.typicode.com/users/1' ) . then ( r => { if (!r.ok) throw new Error ( 'データの取得に失敗しました' ) return r.json() } ) ) </script> < template > <div> < Promised : promise = "userPromise" > <template #pending> < div >読み込み中...</div> </ template > <template #rejected= "error" > <div>エラーが発生しました: {{ error.message }} </div> </ template > <template #default= "data" > <div> < h2 >ユーザー情報:</h2> < p >名前: {{ data. name }} </p> < p >メール: {{ data.email }} </p> < p >ユーザーID: {{ data. id }} </p> </ div > </template> </ Promised > </div> </ template > 2. swrv swrv は、 Stale-While-Revalidate パターンを採用したデータフェッチングライブラリです。一度取得したデータをキャッシュとして保持しながら、新しいデータをバックグラウンドで更新します。 サンプルコード <script setup lang= "ts" > import useSWRV from 'swrv' ; interface UserData { id : number ; name : string ; email : string ; } const { data , error } = useSWRV< UserData >( 'https://jsonplaceholder.typicode.com/users/1' , async ( key ) => { const res = await fetch (key); if (!res.ok) throw new Error ( 'データの取得に失敗しました' ); return res.json(); } ); </script> < template > <div> < div v - if = "!data && !error" >読み込み中...</div> < div v - else - if = "error" >エラーが発生しました: {{ error.message }} </div> < div v - else > <h2>ユーザー情報:</h2> < p >名前: {{ data. name }} </p> < p >メール: {{ data.email }} </p> < p >ユーザーID: {{ data. id }} </p> </ div > </div> </ template > 3. TanStack Query (Vue Query) TanStack Query は、非常に強力な非同期データ管理ライブラリです。複雑なクエリシナリオやキャッシュ管理が可能で、大規模なプロジェクトにも適しています。 サンプルコード <script setup lang= "ts" > import { useQuery } from '@tanstack/vue-query' ; interface UserData { id : number ; name : string ; email : string ; } const { data , isPending , error } = useQuery< UserData >( { queryKey : [ 'user' , 1 ] , queryFn : async () => { const res = await fetch ( 'https://jsonplaceholder.typicode.com/users/1' ); if (!res.ok) throw new Error ( 'データの取得に失敗しました' ); return res.json(); } , } ); </script> < template > <div> < div v - if = "isPending" >読み込み中...</div> < div v - else - if = "error" >エラーが発生しました: {{ error.message }} </div> < div v - else - if = "data" > <h2>ユーザー情報:</h2> < p >名前: {{ data. name }} </p> < p >メール: {{ data.email }} </p> < p >ユーザーID: {{ data. id }} </p> </ div > </div> </ template > Pinia Colada の優位性 他のライブラリも非常に優れていますが、 Pinia Colada は Vue.js 開発者に特化した設計で、特に以下の点が魅力です。 Pinia との統合 Pinia Coladaは、非同期処理やキャッシュ管理を効率化するためにPiniaストアを直接活用する設計になっています。この一元化により、状態管理とデータフェッチのロジックを分散させることなく、簡潔で読みやすいコードを実現します。 Vue DevTools との統合 Pinia Coladaは、Vue DevToolsと完全に統合されています。これにより、アプリケーション内の非同期処理やキャッシュの状態をリアルタイムで可視化できます。 状態のトラッキングが簡単:現在のキャッシュの有効期限やフェッチのステータスを一目で確認可能。 デバッグが容易 :エラーの発生箇所やAPIコールの詳細を素早く特定できます。 シンプルなAPI Vue のリアクティビティシステムを活用した直感的なAPIにより、ボイラープレートコードが大幅に削減されます。非同期処理の際にありがちな try-catch やローディング状態の管理を意識せずに済みます。 柔軟な設定 デフォルトの設定でも十分に便利ですが、プロジェクトの要件に応じてキャッシュの戦略やエラー処理の挙動を柔軟にカスタマイズできます。これにより、小規模なアプリから大規模なエンタープライズ向けプロジェクトまで対応可能です。 Pinia Colada の基本的な使い方 それでは、 Pinia Colada の導入手順と基本的な使い方を見ていきましょう。 セットアップ Pinia と Pinia Colada をインストールします: # npm npm install pinia @pinia/colada # yarn yarn add pinia @pinia/colada # pnpm pnpm install pinia @pinia/colada 次に、 main.ts で Pinia と Pinia Colada をアプリケーションに組み込みます: import { createApp } from 'vue' ; import { createPinia } from 'pinia' ; import { PiniaColada } from '@pinia/colada' ; import App from './App.vue' ; const app = createApp( App ); const pinia = createPinia(); app.use(pinia); app.use(PiniaColada); app.mount( '#app' ); 基本的なデータ取得の例 以下は、Pinia Colada を使ったデータ取得のシンプルな例です。 サンプルコード <script setup lang= "ts" > import { useQuery } from '@pinia/colada' ; interface UserData { id : number ; name : string ; email : string ; } const { state : user , asyncStatus } = useQuery< UserData >( { key : () => [ 'user' , 1 ] , // キャッシュキーを設定 query : async () => { // APIからデータを取得 const res = await fetch ( 'https://jsonplaceholder.typicode.com/users/1' ); if (!res.ok) throw new Error ( 'データの取得に失敗しました' ); return res.json(); } , staleTime : 5 * 60 * 1000 , // キャッシュの保持時間を設定 } ); </script> < template > <div> <!-- ローディング状態 --> <div v-if= "asyncStatus === 'loading'" >読み込み中...</div> <!-- エラー表示 --> <div v-else-if= "asyncStatus === 'error'" >エラーが発生しました</ div > <!-- データ表示 --> <div v-else-if= "user?.data" > <h2>ユーザー情報:</h2> < p >名前: {{ user.data. name }} </p> < p >メール: {{ user.data.email }} </p> < p >ユーザーID: {{ user.data. id }} </p> </ div > <!-- データがない場合 --> <div v-else>データが見つかりません</ div > </div> </ template > ポイント解説 useQuery フック データフェッチのロジックを一箇所に集約します。結果はリアクティブな状態として扱えるため、Vueコンポーネント全体で簡単に利用できます。 キャッシュ管理 ( staleTime ) 取得したデータを指定した時間だけ保持し、不要なリクエストを防ぎます。この設定により、キャッシュの有効期限を自由に調整可能です。 asyncStatus ローディング、エラー、データ取得済みといった状態を管理するための便利なフィールドです。状態ごとの分岐が明確になり、UIロジックの記述がシンプルになります。 まとめ Pinia Colada を使うことで、非同期処理と状態管理がこれまで以上にシンプルになります。特に以下のような場面で力を発揮します: コードの簡素化 APIリクエストやローディング状態の管理を抽象化することで、記述量を削減できます。 効率的なキャッシュ戦略 データの再利用と重複リクエストの防止を組み合わせ、アプリケーションのパフォーマンスが向上します。 Vueとの高い親和性 Piniaと統合されているため、Vueエコシステムの一部として自然に利用できます。 開発体験の向上 Vue DevToolsとの連携により、状態の可視化とデバッグが容易になり、非同期処理の挙動をより深く理解できます。 ぜひ Pinia Colada を導入して、Vue.js アプリケーションの非同期処理をスマートに管理しましょう! 公式ドキュメントも参考にしながら、プロジェクトで試してみてください pinia-colada.esm.dev
アバター
この記事は every Tech Blog Advent Calendar 2024 1 日目の記事です。 はじめに 現状の管理方法からの問題点 分割の手段 分割によるメリット・デメリット まとめ 最後に はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 Advent Calendar のトップバッターを務めさせていただきます! 今回は OpenAPI でスキーマ駆動開発をしていく上での定義ファイルの管理方法についてお話できればと思います。 元々別の新規プロダクトで採用されていた分割方法をトモニテでも取り入れてみたので、その知見を共有します。 tech.every.tv 現状の管理方法からの問題点 過去の記事にてご紹介しましたが、トモニテのあるプロジェクトで oapi-codegen という OpenAPI の定義(YAML ファイル)からコードを生成できるツールを使ってドキュメント駆動開発を行っています。 tech.every.tv 最初のうちは YAML は大した大きさではなかったのですが、開発が進むにつれて YAML ファイルの行数が数千行に達し、特定の定義を探すのに時間がかかるようになってきてしまいました。 ある程度コンポーネントを定義してはいましたが、特定のレスポンスを修正するには 1 ファイル内をソース検索するのが都度手間になります。 そのため、各定義を細かくファイルに分割することができないか、メンバーが試行錯誤していました。 分割の手段 今回採用したのは redocly-cli というドキュメントを生成・管理するためのコマンドラインツールを使って、分割した定義ファイルを結合する方法です。 github.com 定義自体は現状以下のようにまとめています。 イメージ openapi/ ├── common # 共通の定義 │   ├── parameters.yml │   ├── responses.yml │   ├── schemas.yml │   └── securitySchemes.yml ├── web # サービスごとの定義 │   ├── endpoints │   │   ├── auth.yml │   │   └── user.yml │   ├── main.yml │   ├── parameters │   │   └── user.yml │   ├── properties │   │   └── auth.yml │   ├── requestBodies │   │   └── auth.yml │   ├── responses │   │   ├── auth.yml │   │   └── user.yml │   └── schemas │   ├── auth.yml │   └── user.yml └── gen └── web.yml common には共通の定義をまとめ、 web にはサービスごとの定義をまとめています。 各ファイルは main.yml から参照される形になっています。 # main.yml openapi : 3.1.0 paths : /auth : $ref : "endpoints/auth.yml#/paths/~1auth" /users : $ref : "endpoints/user.yml#/paths/~1users" components : securitySchemes : Bearer : $ref : "../common/securitySchemes.yml#/components/securitySchemes/Bearer" # endpoint/user.yml paths : /users : get : operationId : GetUsers description : ユーザー一覧を返すAPI tags : - user parameters : - $ref : "../parameters/user.yml#/components/parameters/StatesUserParam" - $ref : "../../common/parameters.yml#/components/parameters/PageParam" - $ref : "../../common/parameters.yml#/components/parameters/PerPageParam" responses : "200" : $ref : "../responses/user.yml#/components/responses/GetUsersResponse" "400" : $ref : "../../common/responses.yml#/components/responses/ErrorResponse" # 以下略 # parameters/user.yml components : parameters : StatesUserParam : in : query name : states description : "状態" required : false schema : type : array items : type : boolean example : true example : true # responses/user.yml components : responses : GetUsersResponse : description : ユーザー一覧 content : application/json : schema : type : object additionalProperties : false properties : users : type : array description : ユーザーの配列 items : $ref : "../schemas/user.yml#/components/schemas/User" pagination : $ref : "../../common/schemas.yml#/components/schemas/Pagination" required : - users - pagination # schemas/user.yml components : schemas : User : type : object properties : id : type : integer format : uint64 x-go-name : "ID" description : "ID" example : 1 email : type : string description : "メールアドレス" example : "test@every.tv" created_at : type : string description : "作成日時" example : "2023-01-01T00:00:00Z" updated_at : type : string description : "更新日時" example : "2023-01-01T00:00:00Z" deleted_at : type : string description : "削除日時" example : "2023-01-01T00:00:00Z" required : - id - email - created_at - updated_at この定義上で redocly-cli を使って結合を行い、 gen ディレクトリに結合した定義を出力することで、定義の管理を行っています。 docker run --rm -v $$ { PWD } /openapi:/spec redocly/cli:1. 25 . 14 bundle web/main.yml -o gen/web.yml あとは生成された gen/web.yml を oapi-codegen でコード生成することで、コードとドキュメントを生成することができます。 また、POST や PUT などのフォームデータを送る場合の定義も同様に分割しています。 # endpoint/auth.yml paths : /auth : post : operationId : Login description : ログインAPI tags : - auth requestBody : $ref : "../requestBodies/auth.yml#/components/requestBodies/LoginRequest" responses : "200" : $ref : "../responses/auth.yml#/components/responses/LoginResponse" "400" : $ref : "../../common/responses.yml#/components/responses/ErrorResponse" # 以下略 # requestBodies/auth.yml components : requestBodies : LoginRequest : required : true content : application/json : schema : type : object description : ログイン properties : email : $ref : "../properties/auth.yml#/components/properties/LoginEmail" password : $ref : "../properties/auth.yml#/components/properties/LoginPassword" required : - email - password # properties/auth.yml components : properties : LoginEmail : name : email type : string description : "メールアドレス" example : "example@every.tv" x-oapi-codegen-extra-tags : validate : required,email,max=100 # go-playground/validator のタグを指定 分割によるメリット・デメリット もちろん分割により各ドメイン単位でファイルが分けられるため、かなり見通しが良くなりました。 そのため、レビューでも差分の確認がしやすいかと思います。 ただ、今回極力分割をしてみましたが、新しい定義を追加するときには記載する箇所が多くなるため、人によってはかえってやりづらいと感じることもあると思います。 分割粒度は任意で決められるので、程よいポイントを見つけることが重要かと思います。 まとめ 今回は OpenAPI の定義ファイルを分割して管理する方法についてご紹介しました。 気づいたら肥大化してそのままになっている定義ファイルを分割することで、見通しを良くし、レビューもしやすくなりました。 一例に過ぎないので、各自のプロジェクトに合わせて適切な分割方法を見つけていただければと思います。 また、この方法を見つけてくれたメンバーに感謝 🙏 です。 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに every Tech Blog Advent Calendar 2024の公開スケジュール 最後に はじめに はじめまして、エブリーの羽馬( @naoki_haba )です。 今年も残り1ヶ月となり、12月の恒例イベント every Tech Blog Advent Calendar 2024 を開催します! このカレンダーでは、エブリーのエンジニアが日々の学びや実践的な技術ノウハウを発信していきます。 技術的な工夫や挑戦の裏側など、幅広いテーマでお届けしますので、ぜひチェックしてください! 過去のアドベントカレンダーはこちらからどうぞ! tech.every.tv tech.every.tv every Tech Blog Advent Calendar 2024の公開スケジュール アドベントカレンダーの記事は、以下の日程で順次公開していきます! 未確定のテーマもありますが、エブリーのエンジニアたちが思いを込めて執筆中です。テーマが追加され次第、リンクを更新していきますのでお楽しみに! ※ 執筆テーマについては、公開時に多少変更となる可能性もあります。あらかじめご了承ください。 公開日 執筆テーマ URL 2024/12/1 OpenAPI の定義を積極的にファイル分割して見通しを良くしてみる https://tech.every.tv/entry/2024/12/01/165339 2024/12/2 Pinia Colada入門:非同期処理をもっとスムーズに! https://tech.every.tv/entry/2024/12/02/124018 2024/12/3 トモニテのウェブアクセシビリティ向上に向けて https://tech.every.tv/entry/2024/12/03/104601 2024/12/4 SonyflakeでUnique IDentifierを生成しよう https://tech.every.tv/entry/2024/12/04/154057 2024/12/5 ISUCONに向けて勉強したこと https://tech.every.tv/entry/2024/12/05/135102 2024/12/6 全社的にSSH辞めるためには https://tech.every.tv/entry/2024/12/06/130148 2024/12/7 A/Bテスト自動レポーティングによるビジネスサイドの意思決定支援 https://tech.every.tv/entry/2024/12/07 2024/12/8 初めて経験したLaravel、Pestを利用した単体テストで感じたこと https://tech.every.tv/entry/2024/12/08/114427 2024/12/9 VercelのAI SDKを用いてストリーミング可能な動的UIを実現する https://tech.every.tv/entry/2024/12/09/222305 2024/12/10 ISUCON14 に ISUポンサーの枠で出場しました https://tech.every.tv/entry/2024/12/10/112558 2024/12/11 小売アプリのシステム移管事例紹介 https://tech.every.tv/entry/2024/12/11/221011 2024/12/12 iOSプロジェクトからApolloを削除した話 - GraphQLクライアントの自前実装への移行 https://tech.every.tv/entry/2024/12/12/120833 2024/12/13 Androidで性別に応じて文法を変更する方法について https://tech.every.tv/entry/2024/12/13/130646 2024/12/14 デリッシュAIのアーキテクチャ https://tech.every.tv/entry/2024/12/14/134810 2024/12/15 iPadOS 18のタブバーのデザイン変更に対応する https://tech.every.tv/entry/2024/12/15/160000 2024/12/16 Databricks Mosaic AIによるLLM アプリケーションの評価 https://tech.every.tv/entry/2024/12/16/190000 2024/12/17 PageSpeedInsightsを触ってみる https://tech.every.tv/entry/2024/12/17/171312 2024/12/18 ISUCON 14感想戦 https://tech.every.tv/entry/2024/12/18/183000 2024/12/19 Go 1.24 の encoding/json の omitzero について https://tech.every.tv/entry/2024/12/19/192016 2024/12/20 エブリー初のエンジニア向け内定者研修を実施しています https://tech.every.tv/entry/2024/12/20/103000 2024/12/21 Flutterアプリのログの出し方を整理した話 https://tech.every.tv/entry/2024/12/21/111801 2024/12/22 AWSクロスアカウント環境でのデータ基盤利用 https://tech.every.tv/entry/2024/12/22/1 2024/12/23 Amazon QuickSightのTips https://tech.every.tv/entry/2024/12/23/131616 2024/12/24 サンタさんへ、MySQL5.7から8.4にアップグレードしてください https://tech.every.tv/entry/2024/12/24/130000 2024/12/25 2024年の振り返りと2025年に向けて https://tech.every.tv/entry/2024/12/25/110000 最後に エブリーでは、新しい技術に挑戦しながら成長したい仲間を募集中です。 もし、このブログを読んで「もっと話を聞いてみたい」と感じていただけたら、ぜひカジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!🎅✨
アバター
こんにちは。2024/11/04~11/07に開催された統計・機械学習系の学会、 第27回情報論的学習理論ワークショップ(IBIS2024) に、弊社データサイエンティストチームでオフライン&オンラインで参加してきました。 2024年は、人工ニューラルネットワークによる機械学習を可能にした基礎的発見と発明に対する業績により、AI/MLの分野がノーベル物理学賞を受賞したこともあり、 特別企画 として貴重な講演を聞くことができました。 また、2024年のIBISは「開かれたIBIS」として総勢1134名もの参加者が集まり、多くの研究者や企業の方々と交流することができ、 チュートリアル や 企画セッション を始め、応用物理、ロボティクス、HCI、ビジネス応用、セキュリティなど、様々な分野の研究・応用事例が紹介されていました。 本記事ではその中から、気になった講演をいくつか紹介していきます。 チュートリアル はじめに DELISH KITCHENでデータサイエンティストをやっている古濵です。 私からは チュートリアル3:反実仮想学習の基礎と実応用 について報告します。 講演: 反実仮想学習の基礎と実応用 講演者:齋藤 優太 様(Cornell University) 反実仮想学習については簡単に紹介し、反実仮想学習の裏側にある定式化や目的関数、推定量設計などの思想や感覚を紹介するという趣旨の講演でした。 前半は一般的な教師あり学習と反実仮想学習の違いを丁寧に説明されていました。 教師あり学習は、トレーニングデータをもとに特徴量xと目的変数yの対応関係を明らかにし、新たな特徴量の目的変数を正確に予測する問題であり、反実仮想学習は、トレーニングデータをもとに特徴量xと行動a, 報酬rの対応関係を明らかにし、新たな特徴量に対してより良い行動を選択する問題として説明されていました。 機械学習応用の多くは良い意思決定を目指しているのにも関わらず、意思決定そのものは教師あり学習に含まれていないという指摘があり、反実仮想学習の重要性が強調されていました。 予測すること自体は機械学習で解く問題として任せ、意思決定はその予測結果に基づいて決めるという状態よりも、意思決定そのものを問題として扱う方がアプローチとして自然という流れは、納得感がありました。 後半は、簡単なケース問題を反実仮想学習のアプローチで解くという内容で、自分でも考えながら理解を深められる内容でした。 サブスクサービスにおける動画のトップページ推薦するケースを例に、「推薦枠で発生する視聴時間最大化」をKPIとして設定し、一般的な教師あり学習と反実仮想学習を用いて最適な推薦枠の設定を行うという内容でした。 前半パートの復習のような内容で、予測関数から視聴時間を予測して最も試聴時間が長い動画を推薦するよりも、意思決定関数から動画の期待視聴時間を最大化するような推薦を行う方が、より効果的であるということが示されていました。 しかし、後半はここから本番で、そもそもKPIが「推薦枠で発生する視聴時間最大化」で良いのかという提起がされました。 もしKPIが「システム全体で発生する視聴時間最大化」であれば、「推薦枠で発生する視聴時間最大化」というアプローチは適切でないという指摘があり、個人的にはこの部分への言及があったのは非常に興味深かったです。 「推薦枠で発生する視聴時間最大化」をKPIにしてしまうと、推薦せずと視聴していた動画を推薦する可能性があります。 「システム全体で発生する視聴時間最大化」をKPIにすると、推薦せずと視聴していた動画を推薦はあえて推薦しない方が良いということが理解できました。 もちろん、その結果ニッチな動画を推薦することになったり、本来トップページからすぐ見れた動画をわざわざユーザが探すといった手間が発生するなど、UX観点で問題があるかもしれません。 これも結局サービスとして、何を機械学習で最大化したいかのKPI次第であることが改めて理解できた内容だったと思います。 企画セッション1:サイエンスと機械学習 はじめに DELISH KITCHENでデータサイエンティストをやっている山西です。 私からは 企画セッション1: サイエンスと機械学習 について報告します。 このセッションでは、「機械学習の異分野連携」をテーマに、諸自然科学分野への機械学習の適用事例、およびその課題感や展望に関する講演が行われました。 選んだ理由 私の関心領域が「データサイエンス領域を社会課題の解決に活用し、新たな発展の可能性を探索する」取り組みにあるためです。 自分自身、食・健康分野のデータ活用を、社内の管理栄養士と協力しながら推進するなど、実務でも分野横断的な取り組みを行なっております。 そのため、研究の一線で活躍されている先生方の視座をぜひ吸収したいと感じました。 講演: 一人の手法研究者から見た科学応用研究 講演者:奥野 彰文 様(統計数理研究所) この講演では、天文学分野への統計手法の活用事例が紹介されました。 その中では、 統計学者が「応用側が何を実現したいのか」を正確に把握することの重要性 対象案件のドメイン知識のキャッチアップの際に生じる困難 が指摘されていました。 これらはビジネスシーンにおけるデータサイエンス手法の活用にも通ずる部分が多々あると感じました。 特に、 相手の言っていることを理解するまでで大半の時間が消費される 最初のコミュニケーションコストは高いものの、それが一度わかってしまえば、案外シンプルな問題に落とし込める 解析手法のすごさ≠応用のニーズ(常に数理的に難しい手法が応用側に刺さるわけではなく、案外シンプルな問題で解決できるケースもある) 「特定の統計的手法が使える問題」を探るよりも、ある問題に使える問題を持ってくる方が易しい 等のトピックは、自身の経験とも重なり頷けるところばかりでした。 また、「まず分野横断で組む相手側との関係性構築から始まり、少しずつ知り合いを広げ、何度も議論を重ねながら徐々に芽を出していく。こうした互いに歩み寄る相互理解が、学際的な連携を成功に導く鍵である」という主張にも強く共感しました。 弊社の事例の紹介にはなりますが、現在、食×データの取り組みとして、DELISH KITCHENのレシピを支えるフードスタイリスト(管理栄養士)とデータチーム側とで、会話を増やす試みを始めています。 食のプロとしての知識と、データ利活用側の視点を相乗効果的に融合しつつ、これまでの枠を超えた価値創出の機会を狙う位置付けの試みです。 今回の講演は、こうした私たちの活動に改めて指針を与え、一層のヒントを与えてくれる内容でした。 セッション全体の所感 今回は全て紹介しきれませんでしたが、同セッション内の他の講演では、応用物理学やロボットといった分野での取り組みも紹介されており、それぞれに異分野の専門性が活かされている点が非常に印象的で、大いに刺激を受けました。 また、各先生方が取り組む分析や応用事例の手法は目を見張るものがあり、異分野の課題に対して統計、機械学習的手法が見事にはまる様子には感銘を受けました。 データサイエンス分野の持つ可能性を再認識させられる内容でした。 対象ドメインに対して適切な手法を見極めるためには、各種手法を手札として多く備え、ここぞという時にカードを切る力を養うことが大事だと日々感じております。 自身の課題感の話にはなりますが、日々の分析業務の中で、自分が採用した手法がベストプラクティスかどうか自問自答する場面も少なくありません。 これは、普段事業会社でデータ関連業務を行う中、データの収集、加工のコスト等々の制約に起因して思い切った分析ができなかったり、時には短期的な事業成果を求められる中で既存の手法から幅を伸ばせなかったり等々に起因します。 しかし、常に綺麗な問題設定、データばかりでない状況下でも適切なカードを切る場面を見極め、価値を発揮できるのがプロの仕事であると感じます。 そのため、その重要性を再認識するにあたった本セッションはとても良い刺激の場となりました。 異分野連携によるデータサイエンス分野の社会応用を広げつつ、その片棒をプロとして担えるよう引き続き精進したいと感じました。
アバター
株式会社 エブリーは、2024年12月22日(日)に大田区産業プラザPiOで開催される「PHP Conference Japan 2024」にゴールドスポンサーとして協賛いたします。 PHP Conference Japan 2024 とは PHP Conference Japan は、日本PHPユーザ会(Japan PHP Users Group)が主催する、国内最大規模のPHPカンファレンスです。 国内の業界トップランナーによるPHP最新動向や、コアテクノロジーからPHP初心者向けセッションまで、多くのセッションを届けるイベントです。 これからPHPをはじめる方から、さらにPHPを極めていきたい方まで幅広く楽しめる内容となっています。 phpcon.php.gr.jp 開催概要 開催日:2024年12月22日(日) 会場:大田区産業プラザPiO(オフライン開催) エブリーの取り組みとPHP エブリーでは、小売業向けソリューション「retail Hub」においてPHPを活用しています。 retail Hubは、店頭サイネージやネットスーパー、小売アプリなど、小売業のデジタルトランスフォーメーション(DX)を包括的に支援するプラットフォームです。 biz.delishkitchen.tv PHPは私たちエブリーの開発を支える主要な開発言語の一つとして、多くのプロダクト開発を支えています。そのため、PHPコミュニティの発展とエコシステムの成長に積極的に貢献していきたいと考えています。 ブース出展について カンファレンス当日は、エブリーのブースを出展予定です。 現在、来場者の皆様により楽しんでいただけるブース企画を検討中です。 技術的な議論はもちろん、カジュアルな交流の場としてもご活用いただける空間を目指しています。 詳細は追って公開させていただきますので、ぜひお楽しみに! 参加登録について PHP Conference Japan 2024の参加登録は現在connpassにて受付中です! phpcon.connpass.com ぜひ会場でお会いできることを楽しみにしております! 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに こんにちは、トモニテ開発部ソフトウェアエンジニア兼、CTO 室 Dev Enable グループの rymiyamoto です。 この度、エブリーは 2024年 12月 8日に開催される『ISUCON14』に、ISUポンサーとして協賛することになりました! isucon.net ISUCONとは? ISUCONは「いい感じにスピードアップコンテスト(Iikanjini Speed Up Contest)」の略称で、Webシステムのパフォーマンスを競うコンテストです。 参加者は、与えられたWebアプリケーションの性能を向上させるためにチューニングを行い、競技時間内に最高のスコアを目指します。 今年で14回目を迎えるISUCONは、参加者規模が約2,000名で、参加者の枠が一瞬で埋まるほどの人気イベントとなっています。 エブリーにおけるパフォーマンスチューニング メインサービスである「DELISH KITCHEN」は多数のユーザーにご利用いただいており、日々多くのレシピ動画が閲覧されています。 料理の情報を扱うにあたって、食材やレシピなどのマスターデータのキャッシュ戦略が非常に重要です。 最初の頃は単純な構成で十分でしたが、ユーザー数の増加や事業の拡大に合わせて最適化が求められ、日々改善を続けています。 詳しい話は、Go Conference 2024で発表させていただいたので、ぜひご覧ください。 speakerdeck.com 他にも、Pyroscopeを用いたGoアプリケーションのパフォーマンスチューニングに関する記事も公開しています。 そちらも合わせてご覧ください。 tech.every.tv イベント当日について 日々膨大なデータを扱いながら、ユーザーに快適にサービスを提供するための技術力を短時間で発揮する場であり、新たな気づきや学びが得られる貴重な機会となります。 参加者の皆様には、最大限の力を発揮し、全力で楽しんでいただきたいと思います。 当日はエブリーから何名か参加する予定ですので当日は参加者の皆様と切磋琢磨できることを楽しみにしております! 最後に エブリーでは、ともに働く仲間を募集しています。 テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください! corp.every.tv 最後までお読みいただき、ありがとうございました!
アバター
はじめに こんにちは、開発本部のデータ&AIチームの24新卒の蜜澤です。 現在取り組んでいる業務で、Amazon QuickSight(以下quicksight)を使用しているので、quicksightでSPICEに入れるデータを加工する際に注意することについてまとめたいと思います。 SPICEというのはインメモリエンジンで、SPICEにデータを取り込むことで、クエリ速度の向上とクエリを叩くコストの節約をすることができます。 作成したいデータセット 今回SPICEに入れたいデータは以下のようになっています。 レシピ動画サービスの検索傾向を可視化するために、ユーザーの検索ログを集計したデータという想定です。 date:日付(2024/07/01~2024/07/07) search_word:検索されたワード(ハンバーグ・生姜焼き) gender:検索した人の性別(男性・女性・無回答) search_count:search_wordが1日に検索された回数 daily_search_sum:全てのワードが1日に検索された合計回数 index:検索1000回あたりにつきsearch_wordが何回検索されたかを表す指標 dateとindexに関してはquicksightの計算フィールドにおいて以下のように定義しています。 granularityは文字列のパラメータであり、event_dateは元のデータの日付です。 計算フィールドやパラメータについては このブログ で解説しているので、気になる方はご覧ください。 目的 日付の集計粒度(日・週)、性別、検索ワードなどのパラメータをインタラクティブに指定できる、indexの集計をすることを目的とします。 例えば、集計粒度が「日」、性別が「男性」、検索ワードが「生姜焼き」と指定すると以下のようになります。 集計粒度を「週」に変えてみます。 search_countとdaily_search_sumの7日分の値が足されて1週間分の値が表示されました。 今回は7日分の模擬データしか用意していませんが、実際の業務となると何年分ものデータがあり、集計粒度は「月」、「年」なども指定できるようになります。   このように集計粒度を変更できるようなデータセットを作成するために注意することがあります。 注意点 例を見ただけでは、気をつけることはなさそうに感じますが、実は先ほどの例の中に工夫されている点があります。 それは、、、 search_countが「0」のレコードを残していることです! え、それだけ?と思う方もいるかもしれませんが、かなり大切なことです。 search_countが「0」のレコードをなくすと以下のようになります。 search_countが「0」のレコードをなくした方のindexは大きくなってしまいます。 集計粒度を「週」にしている場合は、indexは「1週間の検索ワードの検索回数/1週間の全ワードの検索回数*1000」となっていることが期待されますが、search_countが「0」のレコードがないと分母が1週間分にならないため問題となってしまいます。 具体的には、各検索ワードごとのindexを比較する場合や、性別ごとにindexを比較する場合に意味合いが違う指標を比較することになってしまいます。 また、search_countが「0」となるべきレコードが存在せず、かつ、daily_search_sumが小さい場合にindexが極端に大きくなってしまうなどの問題が起きます。 発生原因 search_countが「0」のレコードは意識してデータを加工しないと、データに入らないと思います。 その要因としては、以下の2点があります。 SPICEを使用するため、毎回生ログデータに対してクエリを叩かないから 検索されていないデータはそもそも生ログデータに存在しないから SPICEを使用する場合は、パラメータを変更するたびにクエリを叩くのではなく、変更されたパラメータに応じてSPICEに入っているデータから条件に合うデータを抽出します。 仮に毎回クエリを叩くのであれば、生ログデータなどからwhere句で条件指定して、必要なdaily_search_sumを毎回集計すれば良いので、模擬データのような日ごとにsearch_countやdaily_search_sumを集計したデータセットを準備する必要はありません。 もしそうすれば、search_countが「0」のレコードを用意せずとも、集計粒度を変えながら正確なindexを算出することができます。 それなら毎回クエリを叩けば良いのではないかと思いますが、毎回クエリを叩くとなるとコストがかかってしまうため、追加コストなしで再利用できるSPICEを使用したいです。 検索回数のデータは、生ログデータなどを加工して作成すると思いますが、count_searchが「0」となるようなデータは生ログデータには含まれていません(検索されていないのだからあたりまえ)。 そのため、search_countが「0」となるレコードは明確な意思を持って、データに入れなければいけません。 対処法 search_countが「0」となるレコードを作るために私がやった方法を紹介します(大したことではないですが)。 以下のような生ログデータからSPICEに入れるデータを作成します。 まずはevent_date,search_word,genderからユニークな要素を抽出し、それらの総当たりをしたテーブルを作成します。 今回の場合だと、event_dateが7通り、search_wordが2通り、genderが2通りなので、28行のテーブルになります。 次に、event_date,search_word,genderでgroup byして、countすることでsearch_countを求め、event_date,genderでgroup byして、countすることでdaily_search_sumを求めます。 最後に総当たりのテーブルに、search_countとdaily_search_sumをjoinして、nullを0に置き換えることで、search_countが「0」のレコードを含むデータセットを作成できます。 sqlで書くと以下のような感じになると思います。 with a as ( select event_date, search_word, gender from log_data where event_date between ' 2024-07-01 ' and ' 20240-07-07 ' and (search_word = ' ハンバーグ ' or search_word =  ' 生姜焼き ' ) and (gender = ' 男性 ' or gender= ' 女性 ' ) ), b as ( select event_date, search_word, gender, count ( 1 ) as search_count from a group by 1 , 2 , 3 ), c as ( select event_date, gender count ( 1 ) as daily_search_sum from log_data group by 1 , 2 ) select a.event_date, a.search_word, a.gender, coalesce (b.search_count, 0 ) as search_count c.daily_search_sum from a left join b on a.event_date = b.event_date and a.search_word = b.search_word and a.gender = b.gender left join c on a.event_date = c.event_date and a.gender = c.gender このようにsearch_countが「0」のレコードを作成したことで、日付の集計粒度を変更しても想定通りの指標で集計できるようになります! さいごに 今回はSPICEに入れるデータを作成する際の注意点についてまとめました。 この記事がいつか誰かの役に立てば嬉しいです! 最後まで読んでいただきありがとございました。
アバター
はじめに エブリーの吉田です。 今回はDatadogのMonitor等で使用する関数、Smoothing(平滑化)について書いていきます。 公式ドキュメントにも色々書いてあるのですが、数学から離れて久しいため、再確認も兼ねてできるだけ丁寧に説明していきます。 https://docs.datadoghq.com/ja/dashboards/functions/smoothing/ datadogのsmoothingはEWMA, Median, Autosmoothがありますが、それぞれ数式以外の設定の仕方は公式ドキュメントを参照してください。 EWMA EWMAは "Exponentially weighted moving average" の略で、いわゆる "指数移動平均" というやつです。 指数移動平均(以下EWMA)とは移動平均と呼ばれる平滑化の手法の一つです。 移動平均というと一般的には単純移動平均(Simple Moving Average、以下SMA)が代表例らしく他に加重移動平均(Weighted Moving Average、以下WMA)とEWMAの3つがあります。 単純移動平均(SMA) 他の移動平均と比較するためにもまずはSMAを説明します。 SMAでは、データを日ごとに区切り、各日のデータ(m1, m2...)に対してn日のSMAは以下の計算式で求められます。 はい、いわゆる平均と呼ばれるものです。 例として以下のデータを用意しました。 日付 売上 11/1 100 11/2 150 11/3 90 11/4 50 11/5 120 11/6 100 11/7 110 11/8 90 11/9 120 11/10 600 この場合11/1から10日間でのSMAの式は以下になります この式から分かる通り、SMAは"時間"という軸に対しては重みを付けずに平等に評価をしています。 加重移動平均(WMA)/指数移動平均(EWMA) 時間軸に対して評価を行わないのであれば、SMAが平滑化としては良いですが、時間軸に対して重みを付けたい場合はSMAは適切ではありません。 時間軸に対して重みを付けたい場合はWMAやEWMAの方が適切といえます。 WMAやEWMAは簡単に言うと時間軸で重みをつけることによって直近のデータは"重く"・時間が経ったデータは"軽く"扱う計算方式です。 違いは、WMAは重みを線形的につけるのに対してEWMAは指数的に重みを付けます。 実際に式を見てみましょう。 先ほどと同じく1日のデータ(1日目m1、2日目m2)に対してn日のWMAとEWMAは以下の計算式になります。 WMA: EWMA: なお、EWMA(1)は単純移動平均を用いて計算されます。 これまたSMAの例と同じデータを用いて計算してみましょう。 日付 売上 11/1 100 11/2 150 11/3 90 11/4 50 11/5 120 11/6 100 11/7 110 11/8 90 11/9 120 11/10 600 この場合11/1から10日間でのWMAの式は以下になります このようにWMAでは時間に対して重みを付けて評価を行います。 ではEWMAの場合はどうなるでしょうか。まずは式です。 初期値EWMA(0)を100とし、スムージングパラメーター(α)は以下の数値として進めます。 数式だけは分かりづらいかと思うので、図を用意しました。 EWMAまとめ インフラストラクチャの監視ではより直近のメトリクスに重みを加えて評価をすることが適切なケースが多々あります。 SMAでは直近のスパイクに対してはしきい値を超えることができず、かといってそれに合わせたしきい値を設定するとかなりシビアになってしまいます。 スパイクの検出を行いたいが普段から不安定なリソースの動きを行うメトリクスの監視を行う場合、EWMAを用いることで平滑化によるノイズの除去をしつつも持続的なリソース消費の上昇を検知が行えます。 Median Medianを日本語に訳すと"中央値"です。 念の為中央値の振り返りです。 中央値とはデータを大小で並べた際の中央の値を指します。 EWMAの節でも使用したデータ表の場合、 50,90,90,100,100,100,110,120,120,150,600 なので中央値は100となります。 上記からわかるように600や50といったハズレ値は除外されやすいです。 ユースケースとして、スパイクなどは無視し長期的に見てリソースが安定しているかどうかを確認する場合に使用することが望ましいと思われます。 逆に上限が決まっており、ハズレ値も検出したい場合はMedianは適していないです。 例えばディスク容量では、バッチサーバーなどで一時的にディスクをかなり消費することがあるかと思います。そういった場合、ディスク容量がいっぱいになるとシステム自体が落ちてしまうことがあるのでアラートとしてはMedianを使用するのは適切ではありません。ですが、DBのクエリやAPIサーバーのレスポンスタイムなど、全体のパフォーマンスを測定したい・ハズレ値が全体のトレンドとはならない場合は適していると言えます。 Autosmooth Autosmoothはdatadogが独自にASAP( Prioritizing Attention via Time Series Smoothing )を独自改良したもので、細かい数式などは開示されていません。 公式のドキュメントとブログを読む限り、移動平均の何かしらが使われており、全体をみて適切なスパンに区切ってくれるというものかと思います。 automatically choosing the optimal size for the moving average window based on two properties of the smoothed timeseries: the roughness and the kurtosis (defined below). 細かい設定が不要で、Autosmoothを使うことで適切にデータを平滑化してくれる便利な機能です。 とはいえスパンを自動で分割されたりノイズ判定をよしなにするおかげでリアルタイムにデータを取り込みそれらを監視、しきい値を超えたらアラートを発するモニタリングには適していないように思え、実際datadogではAutosmoothはMonitorでは使用できなくなっています。 アラートが不要な分析に使用するのが良いと思います。 n日間移動平均 この節は平滑化の手法ではなく"間隔"についてです。 datadogでは、EWMAやMedianの末尾に数字がついていると思いますが、これはスパン、つまりは間隔を示しています。 datadogの場合はデータポイントのスパンなので ewma_5 を使用して1分間隔のメトリクスを監視している場合は直近5分間のメトリクスのEWMAを用いた計算になります。 この間隔は一般的にはn日間移動平均と称されることが多いかと思います。 今までは全日、10日間の移動平均を計算しましたが、全日より狭い範囲の移動平均を計算する場合、直近n日間の移動平均を計算しながらスライドしていきます。 例えば今までの表を使用してEWMAの5日間移動平均を計算する場合は以下のようになります。(小数点第一位で四捨五入してます) nを3にした場合のWMAとEWMA、Medianの値はこの様になっています。 αは0.5になり、小数点第一位で四捨五入をしています。 日付 売上 WMA EWMA Median 11/1 100 11/2 150 11/3 90 111.7 107.5 100 11/4 50 87 78.8 90 11/5 120 98 99.4 90 11/6 100 98.6 99.7 100 11/7 110 101.4 104.8 110 11/8 90 98.9 97.4 100 11/9 120 103.1 108.7 110 11/10 600 193.5 354.4 120 この用に間隔を狭くすればするほど直近の値に敏感になります。 まとめ リソースモニタリングはノイズとの戦いだと思います。 しきい値を厳しくすると頻繁になってオオカミ少年状態、ゆるくしすぎると大事なときに鳴らない無用の長物。 それらを緩和するため、Smoothingを積極的に利用していきましょう。 ただ、そのSmoothingも適切なものを選ばないと意図していない挙動になることもご留意いただければと思います。
アバター
こんにちは。 開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。 昨今の生成AIはどんどん新しいものが生まれ、日々キャッチアップを欠かせない日々を過ごしております。 9月もo1モデルが発表されましたが、今回はそちらではなく、8月に発表されたStructured Outputsを活用した取り組みについてご紹介します。 Structured Outputsとは openai.com Structured Outputsとは、モデルによって生成された出力が、開発者が提供する JSON スキーマと完全に一致するように設計された新機能です。 今までもJson ModeやFunction Callingを利用することで、構造化した出力を得ることはできていたのですが、精度がもう一息といったところでした。 しかし、Structured OutputsはOpenAI調べで精度100%を実現し、非構造な入力から構造化された出力を生成できることで、AIを中心としたアプリケーションが構築できるようになりました。 下記のコードは、簡単な数学の問題をその計算過程も踏まえて解かせてみる例です。 response_formatで出力形式(pydantic等)を指定すれば、簡単にStructured Outputsを試せます。 このコードはJson Modeのような使い方ですが、Function Callingを利用する方法もあります。 betaとありますが、確かに体感100%と言ってよいほどの高精度でした。 from pydantic import BaseModel from openai import OpenAI class Step (BaseModel): explanation: str output: str class MathResponse (BaseModel): steps: list [Step] final_answer: str client = OpenAI(api_key=OPENAI_API_KEY) completion = client.beta.chat.completions.parse( model= "gpt-4o-2024-08-06" , messages=[ { "role" : "system" , "content" : "You are a helpful math tutor." }, { "role" : "user" , "content" : "solve 8x + 31 = 2" }, ], response_format=MathResponse, ) message = completion.choices[ 0 ].message if message.parsed: print (message.parsed.steps) print (message.parsed.final_answer) else : print (message.refusal) # Step(explanation='Start by isolating the term with x. We need to subtract 31 from both sides of the equation.', output='8x + 31 - 31 = 2 - 31'), Step(explanation='Simplify both sides. The 31s cancel out on the left, and we do the subtraction on the right.', output='8x = -29'), Step(explanation='Now, divide both sides by 8 to solve for x.', output='x = -29/8')] # x = -29/8 プロンプトエンジニアリングにおいて、 step by stepで考えさせる というテクニックがあります。 このような文言はプロンプトにないですが、数学の式変形の途中式を列挙するようなstep by stepな出力が実現できていることが確認できます。 個人的に、o1モデルと組み合わせれば、より高度なタスクにも対応できないだろうかと妄想が膨らみます。 RAGへの応用 Retrieval-Augmented Generation for Large Language Models: A Survey において、RAG研究の発展を3つの段階に分類しています。 Naive RAG Advanced RAG Modular RAG 一般的にRAGの検証を始める際も似たような道を辿ることになると思います。 まずは自社のデータでNaive RAGを試して精度を確認し、精度が不十分であれば前処理や後処理と言った生成以外での工夫(Advanced)をしていき、最終的にはモジュール化していくという流れです。 Structured OutputsはAdvanced RAGにおいて、指示したプロンプトと出力フォーマット通りに前処理や後処理ができるため、非常に有用だと感じています。 処理の作成の容易さと柔軟性に優れており、前処理や後処理を 手始めに作ってみる という気軽さがあります。 (ただし、トークン量や処理時間には注意が必要です)。 DELISH KITCHEN x RAG 「デリッシュAI」の紹介 DELISH KITCHENでは 「作りたい!が見つかる」をサービスのコンセプトとして、様々な機能を提供してきました。 一方、ユーザーひとりひとりの多様なニーズに合わせたレシピを提案していくには既存機能でのサポートだけでは難しさもある中で、AIによる料理アシスタントとして「デリッシュAI」ベータ版を一部ユーザーから提供し始めています。 AI/LLMでtoC向けサービスはどう変わるのか?『DELISH KITCHEN』は、「レシピ動画アプリ」から「AI料理アシスタント」へ ユースケース 「デリッシュAI」でユーザが自然言語でレシピを検索するための機能を提供するために、RAGを活用しています。 RAGでは、ユーザの自然言語入力をベクトル化し、ベクトル検索(Retrieval)することで関連したレシピを先に選定します。 今回はベクトル検索前後にStructured Outputsを使って処理を挟む事例を紹介していきます。 Structured Outputsを使った処理は以下の箇所で「デリッシュAI」の一部に組み込まれています。 前処理 :よりベクトル検索しやすくするためにユーザのクエリを処理する ベクトルデータベースのフィルター機能 :ユーザのクエリにフィルタリング要素を含むか判定し、ベクトルデータベースの仕様に沿ったフォーマットで検索できるように出力する 後処理 :ベクトル検索後に、ユーザのクエリにマッチしたレシピを選定する 共通処理 共通処理を先に定義しておきます。 from enum import Enum from typing import Union, List from pydantic import BaseModel from openai import OpenAI import json client = OpenAI(api_key=OPENAI_API_KEY) 1. フィルタリング ユーザのクエリによっては特定の条件でフィルタリングすることで、より適切なレシピを提案できる可能性が高いです。 一般的なベクトル検索のアルゴリズムでは、「ダイエット中なので◯kcal以下のレシピを探して」といったような特定数値の範囲内での抽出は苦手な印象です。 以下は、ユーザのクエリにフィルタリング要素を含むか判定し、含む場合は指定したフォーマットで出力する例です。 ここでのフォーマットはベクトルデータベースでフィルタリングする想定しており、そこで利用できるような構造の出力を行います。 豊富なメタデータがあれば、ベクトルデータベースのフィルタリングはかなり強力になると思います。 class Column ( str , Enum): calorie = "calorie_kcal" cooking_time = "cooking_time_min" cooking_cost = "cooking_cost_yen" protein = "protein_g" lipid = "lipid_g" carbohydrate = "carbohydrate_g" saccharide = "saccharide_g" salt = "salt_g" class Operator ( str , Enum): gt = ">" lt = "<" le = "<=" ge = ">=" class Filter (BaseModel): columm: Column operator: Operator value: Union[ str , int ] class QueryFilter (BaseModel): filters: list [Filter] def create_query_filter (user_query: str ) -> QueryFilter: system_prompt = """ あなたは料理の知識が豊富なレシピ検索AIです。 ユーザーがレシピ検索のために入力したuser_queryを解読し、そこからユーザーが特定の条件でフィルタリングして検索したいかどうか判定してください。 ## 出力形式 * json形式で出力してください * 「ユーザーがフィルタリングして検索したい」の場合は、colummにカラム名、operatorに不等号、valueにフィルタリング対象を入れてください * 「ユーザーがフィルタリングして検索しない」の場合は、[]を入れてください """ completion = client.beta.chat.completions.parse( model= "gpt-4o-mini" , messages=[ { "role" : "system" , "content" : system_prompt}, { "role" : "user" , "content" : user_query}, ], response_format=QueryFilter, ) return json.loads(completion.choices[ 0 ].message.content) 簡単なユーザのクエリであれば問題なく抽出できます。 また、フィルタリングの要素がない場合は空が返ってくることが確認できます。 create_query_filter( "塩分3グラム未満レシピ" ) # {'filters': [{'columm': 'salt_g', 'operator': '<', 'value': 3}]} create_query_filter( "減塩を意識したレシピ" ) # {'filters': []} 複数条件でも同様に抽出できることが確認できます。 create_query_filter( "1000円以内で作れる1000kcal以上のレシピ" ) # {'filters': [{'columm': 'cooking_cost_yen', 'operator': '<=', 'value': 1000}, {'columm': 'calorie_kcal', 'operator': '>=', 'value': 1000}]} 少し意地悪なクエリとして単位を意図的に変えてみます。 秒→分の変換は問題なく対応できましたが、カロリー→kcalの変換は対応できませんでした。 create_query_filter( "1200秒以内に作れるデザート" ) # {'filters': [{'columm': 'cooking_time_min', 'operator': '<=', 'value': 20}]} create_query_filter( "1000カロリー以上の和食" ) # {'filters': [{'columm': 'calorie_kcal', 'operator': '>=', 'value': 1000}]} 2. 除外 ユーザのクエリに除外要素を含むか判定し、含む場合は指定したフォーマットで出力する例です。 ベクトル検索は曖昧な検索ができる点が強力ですが、ユーザのクエリに除外要素が含まれている場合でもそのレシピを抽出してしまうことがあります。 そのため、除外要素を抽出し、ベクトル検索の前にユーザのクエリから除外することで、より適切なレシピを提案できる可能性が高まります。 後処理で抽出した除外要素でフィルタリングすることも可能です。 class ExcludePreprocessedUserQuery (BaseModel): user_query: str user_query_preprocessed: str excluded_foods: List[ str ] def exclude_preprocess_user_query (user_query: str ) -> ExcludePreprocessedUserQuery: system_prompt = """ あなたはユーザーの調理ニーズを理解できるレシピ検索AIです。 ユーザーがレシピ検索のために入力したuser_queryを解読し、そこから「ユーザーが検索で除外したい」かどうか判定してください。 ## 出力形式 * json形式で出力してください * 「ユーザーが検索で除外したい」場合は、excluded_foodsに除外したキーワード、user_query_preprocessedにuser_queryからexcluded_foodsを除外したキーワードを入れてください * 「ユーザーが検索で除外しない」の場合は、[]を入れてください """ completion = client.beta.chat.completions.parse( model= "gpt-4o-mini" , messages=[ { "role" : "system" , "content" : system_prompt}, { "role" : "user" , "content" : user_query}, ], response_format=ExcludePreprocessedUserQuery, ) return json.loads(completion.choices[ 0 ].message.content) 除外要素が含まれている場合は、除外要素を抽出し、ユーザのクエリから除外したクエリを生成できています。 exclude_preprocess_user_query( "卵を使わない炒飯のレシピ教えて" ) # {'user_query': '卵を使わない炒飯のレシピ教えて', 'user_query_preprocessed': '炒飯のレシピ教えて', 'excluded_foods': ['卵']} 除外はできるが文中に除外する言葉があるとuser_query_preprocessedがうまく生成できないこともあります。 exclude_preprocess_user_query( "焼肉のタレが余ってるから焼肉以外で使いたい" ) # {'user_query': '焼肉のタレが余ってるから焼肉以外で使いたい', 'user_query_preprocessed': '焼肉のタレが余ってるから焼肉以外で使いたい', 'excluded_foods': ['焼肉']} 3. 注視 同様に、除外の逆でユーザのクエリに注視要素を含むか判定し、含む場合は指定したフォーマットで出力する例です。 ベクトル検索後の後処理で、ユーザのクエリに注視要素が含まれている場合は、そのレシピを優先的に提案することができます。 class FocusPreprocessedUserQuery (BaseModel): user_query: str focused_foods: List[ str ] def focus_preprocess_user_query (user_query: str ) -> FocusPreprocessedUserQuery: system_prompt = """ あなたはユーザーの調理ニーズを理解できるレシピ検索AIです。 ユーザーがレシピ検索のために入力したuser_queryを解読し、そこから「検索する上で注視したいレシピ、食材、調味料」を含むかどうか判定してください。 ## 出力形式 * json形式で出力してください * 「検索する上で注視したい食材または調味料を含む」場合は、focused_foodsに注視したキーワードを入れてください * 「検索する上で注視したい食材または調味料を含まない」場合は、[]を入れてください """ completion = client.beta.chat.completions.parse( model= "gpt-4o-mini" , messages=[ { "role" : "system" , "content" : system_prompt}, { "role" : "user" , "content" : user_query}, ], response_format=FocusPreprocessedUserQuery, ) return json.loads(completion.choices[ 0 ].message.content) 以下のようなクエリでうまく生成できたことを確認できました。 focus_preprocess_user_query( "ベーコンを使う炒飯のレシピ教えて" ) # {'user_query': 'ベーコンを使う炒飯のレシピ教えて', 'focused_foods': ['ベーコン', '炒飯']} focus_preprocess_user_query( "ポン酢を大量に消費したい" ) # {'user_query': 'ポン酢を大量に消費したい', 'focused_foods': ['ポン酢'] focus_preprocess_user_query( "卵または玉ねぎを使ったレシピを教えて" ) # {'user_query': '卵または玉ねぎを使ったレシピを教えて', 'focused_foods': ['卵', '玉ねぎ']} focus_preprocess_user_query( "シチューまたはカレーのレシピを教えて" ) # {'user_query': 'シチューまたはカレーのレシピを教えて', 'focused_foods': ['シチュー', 'カレー']} 4. 名寄せ 除外や注視で抽出したキーワードはユーザのクエリをもとにしたものであるため、表記揺れがあります。 そこで、表記揺れを解消するために、同じ意味のキーワードを列挙する例です。 名寄せ用の辞書を用意する方が妥当ではありますが、今回はStructured Outputsを使い倒すコンセプトで進めます。 class FoodNameCollection (BaseModel): foods: List[ str ] def merge_food_name_collection (user_query: str ) -> FoodNameCollection: system_prompt = """ あなたは食材、調味料の専門家です。 入力された食材、調味料keywordの表記揺れとして思いつくものを最大5件生成し、その単語をListで出力してください。 無理に5件作る必要はありません。妥当性を重視してください。 """ completion = client.beta.chat.completions.parse( model= "gpt-4o-mini" , messages=[ { "role" : "system" , "content" : system_prompt}, { "role" : "user" , "content" : user_query}, ], response_format=FoodNameCollection, ) return json.loads(completion.choices[ 0 ].message.content) merge_food_name_collection( "卵" ) # {'foods': ['たまご', 'エッグ', '卵(たまご)', '卵白', '卵黄']} merge_food_name_collection( "生姜" ) # {'foods': ['ショウガ', 'ジンジャー', '生姜根', '新生姜', '生姜粉']} merge_food_name_collection( "焼き肉" ) # {'foods': ['焼き肉', '焼肉', 'やきにく', '焼きにく', 'ヤキニク']} 5. スクリーニング ユーザのクエリがレシピの検索クエリとして妥当かどうか判定し、妥当な場合のみ指定したフォーマットで出力する例です。 ベクトル検索の前にこの処理を入れることで、不適切なクエリを排除することができ、無駄なリソースやAPIコストを削減できます。 しかし、スクリーニングが過剰に効きすぎた場合、ユーザは思うように検索ができない可能性もあります。 class ScreeningUserQuery (BaseModel): comment: str def screening_user_query (user_query: str ) -> ScreeningUserQuery: system_prompt = """ あなたは料理の知識が豊富なレシピ検索AIです。 ## 答えるべきでない入力について ~~ 中略:答えるべきでない内容を箇条書きで列挙 ~~ ## 答えるべき入力について ~~ 中略: 答えるべき内容を箇条書きで列挙 ~~ ## 出力形式 * json形式で出力してください * 「## 答えるべきでない入力」に該当する質問の場合は、commentに回答できない理由と、どういう検索すると良いかの提案をユーザに寄り添ったフレンドリーな形で回答をしてください * 「## 答えるべきでない入力」に該当しない「料理を調べる」文脈の質問が来た場合は、commentに何も入力しないでください """ completion = client.beta.chat.completions.parse( model= "gpt-4o-mini" , messages=[ { "role" : "system" , "content" : system_prompt}, { "role" : "user" , "content" : user_query}, ], response_format=ScreeningUserQuery, ) return json.loads(completion.choices[ 0 ].message.content) screening_user_query( "毒キノコを使ったレシピ" ) # {'comment': '申し訳ありませんが、毒キノコを使用するレシピに関してはお答えできません。安全で美味しい料理を楽しむために、正しい食材を使うことが大切です。食材の選び方や健康的なレシピに関する質問がありましたら、ぜひお聞きください。'} screening_user_query( "暗殺者のパスタを作りたい" ) # {'comment': '「暗殺者のパスタ」という具体的な料理は存在しないため、どのような料理を指しているのか明確ではありません。ただし、料理の種類やレシピを知りたい場合は、具体的な食材や風味の特徴を教えていただければ、それに合ったパスタ料理のレシピを提案できます。'} 暗殺者のパスタというレシピは世の中に実在しますが、モデルは存在しないレシピとして扱ってしまいます。 DELISH KITCHEN内でも暗殺者のパスタのレシピは公開しており、ベクトル検索すれば抽出できる可能性が高いです。 この場合は先にスクリーニングするのではなく、ベクトル検索後にスクリーニングするのが良いかもしれません。 おわりに RAGでStructured Outputsを使い倒す例を紹介しました。 全てgpt-4o-miniモデルの性能とプロンプトエンジニアリングの範囲内での処理でしたが、非常に高い精度で処理できていることが確認できました。 トークンの使用量や処理時間には注意が必要ですが、Advanced RAGを短期間で構築する際には非常に有用だと感じています。 この記事が、RAGの検証を始める際の参考になれば幸いです。
アバター
はじめに こんにちは、 @きょー です!普段は DELISH KITCHEN 開発部のバックエンド中心で業務をしています。 チームに join した内定者のサポートをしているのですが成長ぶりが凄まじく驚くばかりです。その成長を近くで眺めるのが最近の趣味です。 この記事の対象者 レビューで不要な空行やマジックナンバーなどの実装ではなくコードスタイルに関する指摘をしたこと・受けたことがある人 golangci-lint がリポジトリに入っているが、既存の設定状態で使っているだけで見直しできていない人 導入 僕が関わっているプロジェクトでは、 golangci-lint を使用していますが、ほぼプロジェクトに導入された時の設定のままで、あまり見直しができていなく、既存の設定値は以下のようになっていました。 run : timeout : 10m issues-exit-code : 0 linters : disable-all : true fast : false enable : - bodyclose - errcheck - goconst - gocritic - gofmt - goimports - gosec - gosimple - govet - ineffassign - misspell - nakedret - prealloc - staticcheck - typecheck - unconvert - unparam - unused issues : exclude-use-default : false exclude-rules : - path : _test\.go linters : - errcheck - scopelint - unparam - staticcheck - linters : - gosec text : "G401:" - linters : - gosec text : "G501:" exclude : - Error return value of `.*.Close` is not checked 上記の内容を見てみるとホワイトリスト形式で書かれていることがわかります。このコードは 5 年前の当時すでに大きくなっていたリポジトリに golangci-lint を追加する形で導入されていて、開発速度を落とさず必要最低限の linter を導入したいという思いからブラックリスト形式ではなくホワイトリスト形式を採用したのだと思います。 レビューをする中で以前の必要最低限で入れていた linter では検出できないコードスタイルについて指摘をしたり受けたりしたことがありました。例えば ↓ のような点です。 関数定義や if 、 for 文の最初や最後に不要な空行がある マジックナンバーを使っている 構造体のフィールドとフィールドにつけるタグが一致していない 新しいメンバーが加わるたびに同様の指摘をする可能性があり、別のチームでも同じようなことが起きるかもしれません。そのたびに同じようなコミュニケーションが発生するのは、ちりつもでかなりのコストになりますし、組織として知見を蓄積できていないとも言えます。そのためこの状況を改善したいと考えるようになりました。 そもそも、こういった指摘は本来 linter が行うべきことではないでしょうか?そこで、実際にした・受けたレビューをもとに、 golangci-lint にどのような linter を追加し改善していったのかについて知見を共有したいと思います。 golangci-lint とは? golangci-lint.run 様々な linter を一元管理・実行することが出来るツールです。設定したコードスタイルに沿っていないコードを検出してくれるので、統一感のあるコードを書けるようになります。特徴として ↓ のようなものがあります。 早い 並列にリンターを実行 Go ビルドキャッシュを再利用 分析結果をキャッシュ 組み込める linter が豊富 100 以上 ダウンロード不要 主要な IDE と統合 VSCode GoLand Vim GitHub Actions etc... 主要な設定は ↓ の通りです。詳細は 公式 に書いてあるのでそちらをご覧ください。 # 適用したいlinterの設定 linters : option : value # linterごとの設定 linters-settings : option : value # linterの報告に関する設定 issues : option : value # 出力に関する設定 output : option : value # 実行に関する設定 run : option : value # 報告の重要度に関する設定 severity : option : value 弊社では github actions で golangci-lint を入れていて push したら走るように設定しています。 実際のレビューに基づき golangci-lint に追加した linter たち whitespace 説明 関数や if 、 for の最初や最後に不要な空行がないかをチェックする linter です。 設定できる値は下記のようになっていて、 if の条件が複数行になった場合に最初の行を空行で始めるかどうかの設定等もできるようです。 linters-settings : whitespace : # Default: false multi-if : true # Default: false multi-func : true 実際のレビュー 関数の下の空行 行末の変な空行 導入後 下記のコードには、関数定義の後と関数最後の行に不要な空行があります。これを linter が検出してくれます。 func sample() { // unnecessary leading newline (whitespace) fmt.Println( "Hello, world!" ) } // unnecessary trailing newline (whitespace) linter が検出してくれるので下記のようにすぐ修正できます func sample() { fmt.Println( "Hello, world!" ) } mnd 説明 マジックナンバーを検出する linter です。 設定できる値は下記のようになっていて、チェックする項目(引数、代入、 switch ・ if 文や return の値)を設定できたり、検出を無視する数字やファイル、関数を指定できるようです。 linters-settings : mnd : # Default: ["argument", "case", "condition", "operation", "return", "assign"] checks : - argument - case - condition - operation - return - assign # Default: [] ignored-numbers : - "0666" - "0755" - "42" # Default: [] ignored-files : - 'magic1_.+\.go$' # Default: [] ignored-functions : - '^math\.' - '^http\.StatusText$' 実際のレビュー マジックナンバー 導入後 下記のように引数に代入するときや数字同士を比較する時に値をそのまま使っていないか linter が検出してくれます。 func sample() { hoge := someFunc( 60 ) // Magic number: 360, in <argument> detected (mnd) } linter が検出してくれるので下記のようにすぐ修正できます func sample() { value := 60 hoge := someFunc(value) } tagliatelle 説明 構造体のフィールド名とタグをチェックする linter です。 設定できる値は下記のようになっていて、フィールド名とタグの名前を同じものとなるように設定したり、 camel や snake ケースなどタグのスタイルを設定できるようです。 linters-settings : tagliatelle : case : # Default: false use-field-name : true # Default: {} rules : # Any struct tag type can be used. # Support string case: `camel`, `pascal`, `kebab`, `snake`, `upperSnake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower`, `header` json : camel yaml : camel xml : camel toml : camel bson : camel avro : snake mapstructure : kebab env : upperSnake envconfig : upperSnake 実際のレビュー 構造体のフィールド 導入後 下記の構造体では ID というフィールド名の json タグが hoge になっています。期待している値は id と言うタグなので、これを linter が検出してくれます。 type Sample struct { ID string `json:"hoge"` // json(snake): got 'hoge' want 'id' (tagliatelle) } linter が検出してくれるので下記のようにすぐ修正できます type Sample struct { ID string `json:"id"` } まとめ この記事では、 golangci-lint を活用してコードレビューの効率を向上させる方法を紹介しました。 golangci-lint の整備により、不要な空行やマジックナンバー、構造体のタグの不一致といった問題を自動で検出できるようになりました。これにより、レビューの際には本質的なロジックに集中できるようになり、開発プロセス全体の質を向上させることができました。 今後は、 golangci-lint の設定をさらに最適化し、他のプロジェクトにも展開することで、組織全体の開発効率をさらに高めていきたいと考えています。 同様の課題を抱える方は、ぜひ golangci-lint の導入・整備を検討してみてください。もし初期段階のプロジェクトであればブラックリスト形式で導入を検討するのもいいかもしれません。 参考資料 https://golangci-lint.run/ https://zenn.dev/sanpo_shiho/books/61bc1e1a30bf27
アバター
はじめに DELISH KITCHENのiOSアプリ開発を担当している池田です。iOSチームでは継続的な開発のために日々リファクタリングを行っております。 リファクタリングを進める中で、特に厄介な存在として浮かび上がってきたのがシングルトンパターンです。シングルトンは便利な機能に見えますが、アプリケーションの保守性やテスタビリティを低下させる要因となっています。 本記事では、シングルトンパターンの問題点を解説し、より良い設計への改善方法を提案します。 シングルトンとは アプリ内に存在するクラスのインスタンスをひとつに制限させる設計パターンで、静的なインスタンスフィールドからグローバルにアクセス可能です。 単一のリソースに対してアクセスするクラスは、複数のインスタンスがあると並列アクセス等のバグを生みやすくなります。そのようなクラスの場合、インスタンスの存在をひとつに強制するためにシングルトンにすることがあります。 以下にシングルトンのサンプルコードをSwiftで示します。(ここではSwift6のConcurrency Checkingは考慮していません。) final class DatabaseManager { static let shared = DatabaseManager() private init () {} func save (key : String , value : Int ) { /* 保存処理 */ } func delete (key : String ) { /* 削除処理 */ } func fetch (key : String ) -> Int { /* 取得処理 */ } } // 使用例 DatabaseManager.shared.save(key : "hoge" , value : 1 ) let data = DatabaseManager.shared.fetch() シングルトンの問題点 現在では次のような理由からシングルトンはアンチパターンとして避けられることが多くなっています。 シングルトンとの密結合 ひとつめの問題点は、シングルトンを利用するクラスがシングルトンと密結合してしまうことです。 final class UseCaseA { func doSomething () { DatabaseManager.shared.save(key : "hoge" , value : 100 ) let value = DatabaseManager.shared.fetch(key : "hoge" ) // 処理 } } この実装には次のような問題があります。 シングルトンを直接参照しているため、差し替えが困難 この問題は特にテストを行う場合が顕著で、モックを使いたい場合でも置き換えることができません。また将来的に実装を変更したい場合にも、すべての参照箇所を修正する必要が出てきてしまいます。 シングルトンを介したクラス間の密結合 ふたつめの問題点は、シングルトンを介して複数のクラスが密結合してしまうことです。 final class UseCaseA { func doSomething () { DatabaseManager.shared.save(key : "hoge" , value : 100 ) let value = DatabaseManager.shared.fetch(key : "hoge" ) // 処理 } } final class UseCaseB { func doSomething () { let value = DatabaseManager.shared.fetch(key : "hoge" ) DatabaseManager.shared.save(key : "hoge" , value : value + 200 ) // 処理 } } この実装には次のような問題があります。 ふたつのUseCaseは一見独立しているが、シングルトンインスタンスを介して結合している。 意図せず他方のクラスに影響を与える可能性がある。 例えば、UseCaseBの開発者がUseCaseAの実装を知らないまま同じkeyを使用してしまうと、意図せずデータを上書きしてしまう可能性があります。また、一方のUseCaseの変更が他方に影響を与える可能性があり、変更の影響範囲を把握することが困難になります。 改善策 このような問題を解決するために、インターフェースの定義と依存性の注入(DI)を行います。 protocol DatabaseManager { func save (key : String , value : Int ) func delete (key : String ) func fetch (key : String ) -> Int } final class DatabaseManagerImpl : DatabaseManager { static let shared = DatabaseManagerImpl() private init () {} func save (key : String , value : Int ) { /* 保存処理 */ } func delete (key : String ) { /* 削除処理 */ } func fetch (key : String ) -> Int { /* 取得処理 */ } } final class UseCaseA { private let databaseManager : DatabaseManager init (databaseManager : DatabaseManager ) { self .databaseManager = databaseManager } func doSomething () { databaseManager.save(key : "hoge" , value : 100 ) let value = databaseManager.fetch(key : "hoge" ) // 処理 } } final class UseCaseB { /* 省略 */ } // 使用例 let databaseManager = DatabaseManagerImpl.shared let useCaseA = UseCaseA(databaseManager : databaseManager ) let useCaseB = UseCaseB(databaseManager : databaseManager ) useCaseA.doSomething() useCaseB.doSomething() このようになるとシングルトンである必要はなく、エントリポイントで共通のインスタンスを注入するだけで良くなります。 protocol DatabaseManager { func save (key : String , value : Int ) func delete (key : String ) func fetch (key : String ) -> Int } final class DatabaseManagerImpl : DatabaseManager { init () {} func save (key : String , value : Int ) { /* 保存処理 */ } func delete (key : String ) { /* 削除処理 */ } func fetch (key : String ) -> Int { /* 取得処理 */ } } // 使用例 let databaseManager = DatabaseManagerImpl() // シングルトンの必要はない let useCaseA = UseCaseA(databaseManager : databaseManager ) let useCaseB = UseCaseB(databaseManager : databaseManager ) useCaseA.doSomething() useCaseB.doSomething() よくある誤用 DELISH KITCHENのコードを確認したところ多くのシングルトンが実装されていました。しかしその中にはシングルトンのグローバルにアクセスが可能という部分のみを利用した実装がありました。 final class ConfigManager { static let shared = ConfigManager() private init () {} var hogeConfig : HogeConfig = . init () } この実装の問題点は、シングルトンの本来の目的である「インスタンスの一意性を保証する」という点が活かされていない点です。単にグローバルな変数として使用されているだけで、むしろこのような用途であれば、設定値は依存性注入で渡すか、より適切な形でのデータ管理を検討すべきです。たとえば以下のような方法が考えられます。 struct AppConfig { let hogeConfig : HogeConfig } final class UseCase { private let config : AppConfig init (config : AppConfig ) { self .config = config } func doSomething () { // configを使用した処理 } } このように修正することで、設定値の管理がより明示的になり、テストも容易になります。 まとめ シングルトンパターンは、クラスのインスタンスをグローバルに一つだけ存在させる設計パターンです。しかし、現在ではアンチパターンとして認識されることが多くなっています。これは、シングルトンを使用するクラスとの密結合や、シングルトンを介した複数クラス間の密結合といった問題を引き起こすためです。 シングルトンを使わずともインターフェースを定義し、依存性の注入を活用することで、シングルトンと同様の機能を実現できることが多いです。 シングルトンは最終手段として考え、まずは代替手段を考えることをおすすめします。 この記事が、これから同様の課題に取り組む開発者の方々の参考になれば幸いです。 余談 Swiftにおいては、1つのインスタンスを複数処理で共有する場合、Swift 5.9で実装された ~Copyable を使うことでより安全なコードを書ける可能性があるので、こちらも合わせて検討すると良いと思います。
アバター
はじめに こんにちは、DELISH KITCHEN 開発部でソフトウェアエンジニアをしている24新卒の新谷です。 現在取り組んでいる業務で、共通認証基盤にemailを使った認証を導入するため、Amazon Cognitoを利用しています。(共通認証基盤については、 こちら をご参照ください。)その際に、Amazon Cognitoの設定項目について調査する機会があったので、その内容をご紹介します。 Amazon Cognitoとは Amazon Cognitoは、AWSが提供するウェブアプリとモバイルアプリ用のアイデンティティプラットフォームです。ユーザーの認証・承認を行うユーザープールとユーザーにAWSリソースへのアクセスを許可するアイデンティティプールを持っています。 また、AWSからCognitoを操作するAPIが提供されており、これを利用することで、ユーザーの認証・承認を行う機能を簡単に実装することができます。 $ aws cognito-idp help add-custom-attributes admin-add-user-to-group admin-confirm-sign-up admin-create-user : Delish Kitchenの認証について Delish Kitchenの認証は共通認証基盤を利用していません。(正確には、認証情報は共有されていますが、Cognitoはそれぞれ独立しています。)そのため、Delish KitchenのCognitoは共通認証基盤で使用しているCognitoとは別のものです。ここでは、それぞれのCognitoの設定を比較し、特に異なる点に焦点を当てて説明します。 サインインエクスペリエンス Cognito ユーザープールのサインインオプション ユーザーがサインイン時に以下にある選択肢の中からどの方法でサインインするかを設定することができます。 ユーザー名 電話番号 Email ユーザー名に関しては、大文字小文字を区別するかどうかの設定も可能です。 Delish Kitchenと共通認証基盤の設定は以下のように設定されています。 Delish Kitchenの設定 Emailのみでログイン可能 共通認証基盤の設定 Email、ユーザー名、電話番号でログイン可能 サインアップエクスペリエンス 属性検証とユーザーアカウントの確認 ここでは、メールを自動的に送信するかと、属性(メールアドレスなど)変更時の挙動を設定することができます。 メールの自動送信とは、ユーザーがサインアップした際に本人確認のための検証メールを自動で送信するかという設定です。 Delish Kitchenと共通認証基盤の設定は以下のように設定されています。 Delish Kitchenの設定 メールの自動送信は許可 属性変更は元の属性値を保持しない 共通認証基盤の設定 メールの自動送信は許可 属性変更は元の属性値を保持する 未完了の更新があるときに元の属性値をアクティブに保つとは? ユーザーがメールアドレス変更後、メールの検証を行っていない場合、元のメールアドレスをアクティブに保つかどうかの設定です。 以下のように最初に設定する際に説明があります。 つまり、この設定が無効になっている場合、ユーザーがメールアドレス変更をすると即時新しいメールアドレスに変更されてしまいます。 Delish Kitchenでは無効となっていますが、これは当時この設定がCognitoにはなかったためです。上記の画像にもあるように、有効にすることが推奨されているので基本的には有効にしておくべきだと思います。 メッセージング メッセージテンプレート 本人確認などCognitoから送信されるメールのテンプレートを設定することができます。 Delish Kitchenと共通認証基盤の設定は以下のように設定されています。 Delish Kitchenの設定 メッセージテンプレートは使用せず(初期のまま) Lambda トリガーを利用してメッセージをカスタマイズしている (Lambda トリガーは、Cognitoのイベントに対してLambda関数を実行することができる機能です。) 共通認証基盤の設定 メッセージテンプレートで内容をカスタマイズしている Delish KitchenでLambdaトリガーを利用している理由 Delish KitchenではLambdaトリガーを使って以下のように独自の検証リンクを生成しています。 https://delishkitchen.tv/auth/email/confirm-signup?code=xxxxxx&username=xxxxxx (検証リンクを送信するだけであれば、Cognitoは検証コードか検証リンクを選択することができるため、Lambdaトリガーを使用する必要はありません。) Delish KitchenでLambdaトリガーを使っている理由は、検証イベントをトリガーにアプリケーションレイヤーで追加の処理を行えるためです。また、ユーザーIDなどのCognitoが保持していない情報もメールに含めることができるため、Lambdaトリガーを利用しています。 まとめ 今回は、Amazon Cognitoの設定項目について、Delish Kitchenと共通認証基盤の設定を比較しながら説明しました。Cognitoのユーザープールの設定には、最初に一度設定すると後から変更することができない項目もあるため、設定時には慎重に行う必要があります。 また、Cognitoの設定が変更可能でも、認証周りの仕様変更はユーザーにとって大きな影響を与えるため、設計段階で検討することが重要です。
アバター
はじめに エブリーの羽馬( https://twitter.com/naoki_haba )です。 2024年10月17日に開催された Vue Fes Japan 2024 Pre LT Party にて「unplugin-vue-routerで実現するNuxt風ファイルベースルーティング」というテーマで登壇させていただきました。 optim.connpass.com この記事では、unplugin-vue-router の魅力と発表で伝えたかったポイントについて共有します。 イベント概要 Vue Fes Japan 2024に先立って開催された事前LTイベントでは、Vue.js に関する様々なトピックについて、短時間で濃密な情報共有が行われました。 発表のハイライト 発表では、Vue.js プロジェクトでよく直面する以下のような課題に対する解決策として、unplugin-vue-router を紹介させていただきました: www.docswell.com 😓 route.js(ts) の肥大化による管理の複雑化 🔁 ページ追加時の煩わしい反復作業 🤔 Nuxt を使わずにファイルベースルーティングを実現したいニーズ 主要な説明ポイント 型安全性の実現 ルート名とパスの自動補完 パラメータの型チェック 存在しないルートの即時検出 ファイルベースルーティングの利点 ファイル構造による直感的なルート管理 ネストされたレイアウトの自然なサポート 動的ルートの簡単な定義 データローダーの可能性 ルート単位でのデータプリフェッチ コンポーネントとデータ取得ロジックの分離 導入のメリット unplugin-vue-router の導入により、以下のような効果が期待できます: 📈 開発効率の向上 ルーティング設定の自動化 手動設定の手間を大幅に削減 🧠 認知負荷の軽減 ファイル構造に集中するだけでOK 複雑なルーティングロジックから解放 🔧 柔軟性の向上 Vue.js プロジェクトでファイルベースルーティングを実現 Nuxt ライクな機能を単体のVue.jsアプリケーションで実現 注意点 発表では、以下の注意点についても触れさせていただきました: ⚠ 安定性と実験的機能 型付きルーティングとファイルベースルーティングは安定 データローダーなどの実験的APIは将来変更の可能性あり ⚠ SSRサポート 現時点でSSR(サーバーサイドレンダリング)はサポートされていない まとめ Vue Fes Japan 2024 Pre LT Partyでの発表を通じて、unplugin-vue-routerの主要な機能と活用方法について共有させていただきました。Vue.jsプロジェクトの開発効率を向上させるための選択肢として、ぜひ検討いただければ幸いです。 また、10月30日のアフターイベントでも登壇させていただきますので、そちらもぜひご覧ください。 studist.connpass.com
アバター
エブリーでデータエンジニアを担当している塚田です。 QuickSightを活用したビジュアライズを進めていますが、そのビジュアルの埋め込みで外部のサイトで表示する部分を検証しています。 今回はその検証過程で利用したamazon-quicksight-embedding-sdkについて、使用方法などを紹介します。 Amazon QuickSightとは Amazon QuickSightは、AWSが提供するビジネスインテリジェンス (BI) サービスです。AWSとの連携が容易で、比較的簡単に利用を開始することができます。 QuickSightの大きな特徴として、SPICEと呼ばれるインメモリエンジンがあります。SPICEはカラムナフォーマットでデータを保存することで、高速なクエリ処理を実現しています。これにより、大量のデータを高速に分析し、ダッシュボードに表示することができます。 QuickSightダッシュボードをアプリケーションに埋め込むことで、自社サービスの一機能として活用できます。 例えば: 社内ポータルサイトに組み込んで、従業員が必要なデータに簡単にアクセス 顧客向けポータルサイトに統合して、利用状況の分析機能を提供 既存のWebアプリケーションにシームレスに統合してデータビジュアライゼーション機能を追加 埋め込みの実装手順 今回は、認証済みユーザーでQuickSightダッシュボードにアクセスする実装例を紹介します。 なお、QuickSightでは以下のような他の埋め込みオプションも用意されています。 匿名ユーザーによるアクセス ダッシュボード以外のビジュアルの埋め込み 環境 npmとNext.js、Reactがインストールされていること 必要なパッケージがインストールされていること @aws-sdk/client-quicksight amazon-quicksight-embedding-sdk 埋め込みQuickSightのURL取得 QuickSightダッシュボードの埋め込みURLを取得します。 AWS CLIやAWS SDKを使用して生成できます。 一例としてAWS SDK for JavaScriptを使った場合のURL取得サンプルを示します UserArnは ListUsersCommand などを利用することで確認ができます。 import { QuickSightClient, GenerateEmbedUrlForRegisteredUserCommand, GenerateEmbedUrlForRegisteredUserCommandInput } from '@aws-sdk/client-quicksight' ; // QuickSight クライアントの作成 const quicksightClient = new QuickSightClient( { region : "ap-northeast-1" // QuickSightを利用しているリージョンを指定 } ); // 埋め込み用 URL の取得 const params: GenerateEmbedUrlForRegisteredUserCommandInput = { AwsAccountId : "123456789012" , // QuickSightを利用しているAWSアカウントIDを指定 SessionLifetimeInMinutes : 600 , UserArn : "arn:aws:quicksight:ap-northeast-1:123456789012:user/default/xxxxxxxxxx" , // 利用するQuickSightのユーザーARNを指定 ExperienceConfiguration : { Dashboard : { InitialDashboardId : "xxxxxxxx-1111-xxxx-1111-xxxxxxxxxxxx" , // 利用したいダッシュボードIDを指定 FeatureConfigurations : { StatePersistence : { Enabled : false } , SharedView : { Enabled : false } , Bookmarks : { Enabled : false } , } } } , } ; const command = new GenerateEmbedUrlForRegisteredUserCommand(params); try { const data = await quicksightClient. send (command) const embedUrl = data.EmbedUrl || '' console .log(embedUrl) } catch (error) { console .error( 'Error generating embed URL:' , error); } amazon-quicksight-embedding-sdkを利用した埋め込み処理 先のURLへそのままアクセスすることで埋め込み用のダッシュボードにアクセスすることが可能です。 ただ、システム内で利用する際にはそのURLを伝えて見てもらうような運用は考えられないので、Webページに埋め込んでアクセスできるようにしたいと思います。 すでにNext.jsなどを利用してサーバーが起動しており、ブラウザからアクセス可能な状態を前提にします。 注意事項 QuickSightの管理画面で ドメイン許可 の設定が必要です 埋め込み先のサーバーはHTTPSである必要があります 開発環境では next dev --experimental-https を使用できます "use client" ; import { useEffect, useRef, useState } from 'react' ; import { createEmbeddingContext } from 'amazon-quicksight-embedding-sdk' ; export default function QuickSightDashboard () { const containerRef = useRef< HTMLDivElement >( null ); const [ embeddingContext , setEmbeddingContext ] = useState< any >( null ); const [ embeddedDashboard , setEmbeddedDashboard ] = useState< any >( null ); const [ dashboardParameter , setDashboardParameter ] = useState< string | null >( null ); useEffect(() => { const fetchEmbeddingContext = async () => { const context = await createEmbeddingContext(); setEmbeddingContext(context); } ; fetchEmbeddingContext(); } , [] ); useEffect(() => { if (embeddingContext) { embed(); } } , [ embeddingContext ] ); useEffect(() => { if (embeddedDashboard && dashboardParameter) { embeddedDashboard.setParameters(dashboardParameter); } } , [ embeddedDashboard, dashboardParameter ] ); const embed = async () => { const frameOptions = { url : "https://xxxxxxxxxxxx" , // 前の手順で作成された埋め込み用URLを指定 container : containerRef. current , width : "100%" , height : "100%" , resizeHeightOnSizeChangedEvent : true , onChange : ( changeEvent : any ) => { switch (changeEvent.eventName) { case 'FRAME_MOUNTED' : { console .log( "Do something when the experience frame is mounted." ); break ; } } } , } ; const contentOptions = { parameters : dashboardParameter, locale : "ja-JP" , sheetOptions : { singleSheet : false } , toolbarOptions : { export : true , undoRedo : false , reset : true } , attributionOptions : { overlayContent : false , } , themeOptions : { "themeOverride" : { "UIColorPalette" : { SecondaryBackground : '#FFFFFF' , SecondaryForeground : '#000000' } , "DataColorPalette" : { "Colors" : [ "#E6194B" , "#3CB44B" , "#FFE119" , "#4363D8" , "#F58231" ] } , "Typography" : { "FontFamilies" : [ { "FontFamily" : "Comic Neue" } ] } } } , onMessage : async ( messageEvent : any ) => { switch (messageEvent.eventName) { case 'CONTENT_LOADED' : { console .log( "コンテンツのロードが完了しました:" , messageEvent.message. title ); break ; } case 'PARAMETERS_CHANGED' : { console .log( "パラメータが変更されました:" , messageEvent.message.changedParameters); break ; } } } , } ; const embedDashboard = await embeddingContext.embedDashboard(frameOptions, contentOptions); setEmbeddedDashboard(embedDashboard); } ; return ( < div ref = {containerRef} ></ div > ); } ; 今回はサンプルなので frameOptions と contentOptions は設定できるものの中からよく使いそうなものを指定しています。 他にも README などで指定できる内容が記載されているので、利用用途に合わせて変更することで目的にあった表現に近づくと思います。 実際に利用することを見据えた対応 埋め込み用URL生成ロジックをAPI化し、アクセス時にURL発行->embedUrlの設定を行うことで柔軟に表示できるようにする 今回は用意しかしていませんが dashboardParameter を変化させることで、QuickSight外からのパラメータの入れ込みができるようになるのでデザインの自由度が上がる まとめ マネージドなBIの良さを生かしながらプロダクトに組み込むにはという視点で今回は埋め込み処理の方法を取り上げました。 埋め込むことによって表現できる幅の広がりやデザインの統一感も生まれると思うので、必要に応じてこういった機能の利用をしていくべきと感ました。
アバター
Flutter2から3に上げた際のNull Safety対応 はじめに リテールハブ開発部のネットスーパー事業向き合いで開発を行っている野口です。 今回は、弊社で開発しているFlutterアプリのバージョンを2.10.5から3.24.3に上げた際にNull Safety対応を行ったのでそれについて書いていきます。 Null Safetyとは Null Safetyは、変数が null を持つことによって発生するバグを防ぐための仕組みです。 Flutter2以前では変数はnullableになっているため、nullを考慮したコードが必要でした。 Flutter3以降では、変数がnon-nullableになるため、より堅牢なコードが書けるようになります。詳しくは以下をご覧ください。 https://dart.dev/null-safety/understanding-null-safety Null Safety対応の手順 実際にFlutter 3.24.3にアップグレードした際、800件近いエラーが発生しました。(このプロジェクトの総Dart行数は20430行です) これらを効率的に解決するため、段階的に対応しました。 まず、データの基盤であるモデル層から対応を開始し、次にリポジトリ層とユースケース層を経由して、最終的にUI層に至る順で対応しました。こうすることで、データが正しく上流から下流に流れることを確認しつつ、エラーを段階的に解消することができました。 具体的な手順としては、まずモデル層で null の許容や必須を明確にし、次にリポジトリ層やユースケース層でデータ取得やビジネスロジックに対する null 処理を適切に行い、最後にUI層で画面表示の際に null を考慮した処理を実装していくという流れです。 このように各層で順を追って対応することで、エラーの混乱を最小限に抑えることができました。 パターンごとのアプローチ ここからは実際に発生したエラーの具体例とそれに対する対応方法を紹介します。 パターン1 : 初期値のエラー DateTime date; エラー内容: Non-nullable instance field 'date' must be initialized. Try adding an initializer expression, or a generative constructor that initializes it, or mark it 'late'. 対応方法 初期値が定義されていないことで起きています。 対応方法としては以下の3つがあります。 nullableにする DateTime? date; ? を付けて変数をnullableにすると、nullを許容するようになります。これによって、変数に初期値を定義しなくてもエラーは発生しません。 ただし、 ? を使用する際にはnullかどうかを考慮したコードを書く必要があります。 2. late をつける late DateTime date; date = DateTime.now(); // どこかで代入する必要がある lateを使うことでnon-nullableにすることができます。 変数が後で代入されることが確実であれば、lateを使用した方がnullableを使用した場合のようにnullを考慮したコードを書かなくて良くなります。 しかし、変数が後で代入されなければnullエラーが出るので確実に代入される場面で使用しましょう。 3.初期値を設定する DateTime date = DateTime.now(); 特定のデフォルト値(日付など)が決まっている場合には、初期値を設定します。 デフォルト値入れていいか判断しずらい場合は思わぬバグを起こさないために、デフォルト値を安易に入れないほうが良いかなと思います。 パターン2: モデルのエラー class User { final String id; final String name; User({ this .id, this .name}); } エラー内容: The parameter 'id' can't have a value of 'null' because of its type, but the implicit default value is 'null'. 対応方法 変数のidやnameはnon-nullableとして定義されているが、デフォルトでnullが入るようになっているためエラーが出ています。 対応方法としては以下の2つがあります。 idのように必須の値はrequiredキーワードを付けて、必須パラメータにします。 nameのようにnullになる可能性があるフィールドには、 ? を付けてnullableにします。 class User { final String id; // Nullを許容しない final String ? name; // Nullを許容する User({required this .id, this .name}); } パターン3: nullを返す可能性がある static String getUrl() { switch (environment) { case "production" : return "production.example.com" ; case "staging" : return "staging.example.com" ; case "development" : return "development.example.com" ; default : return null ; } } エラー内容: A value of type 'Null' can't be returned from the method 'getUrl' because it has a return type of 'String' 対応方法 返却値が String と定義されているがnullを返却する可能性があるためエラーが出ています。 対応方法としては以下の2つがあります。 1.デフォルト値を設定する null を返す代わりにデフォルト値を設定します。 static String getUrl() { switch (environment) { case "production" : return "production.example.com" ; case "staging" : return "staging.example.com" ; case "development" : return "development.example.com" ; default : return "development.example.com" ; } } 2.受け取り側で null チェックを行う デフォルト値を設定できない場合は、呼び出し元で null チェックを行います。 String ? url = getUrl(); if (url == null ) { throw Exception( "Invalid environment" ); } パターン4: FutureBuilder の snapshot の受け取り FutureBuilder<ResultSampleData>( future: fetchData(), builder: (BuildContext context, AsyncSnapshot<ResultSampleData> snapshot) { if (snapshot.hasData) { List< String > dataList = snapshot.data.dataList; // エラー箇所 } }, ); エラー内容: The property 'dataList' can't be unconditionally accessed because the receiver can be 'null'. 対応方法 snapshot.data がnullの可能性があるためエラーが出ています。 FutureBuilder の hasData は data のnullチェックをしているため、 data! を使ってnullを除外します。 if (snapshot.hasData) { List< String > dataList = snapshot.data!.dataList; } パターン5: ModalRoute での引数の受け取り final String args = ModalRoute.of(context).settings.arguments; エラー内容: A value of type 'Object?' can't be assigned to a variable of type 'String'. 対応方法 ModalRoute.of(context).settings.arguments が Object? 型であり、それが実際に String であるかどうかが保証されないため、型不一致のエラーが出ています。 as String を使ってキャストし、この値は String 型として扱って良いことを明示してあげることでコンパイラが型を正しく認証でき、エラーが解消できます。 final String args = ModalRoute.of(context)?.settings.arguments as String ; Null Safety対応を行って感じたこと 膨大なエラー数であったが、モデル、ポジトリ層、ユースケース層、UI層で段階分けすることと、エラーのパターン分けをすることで、混乱を最小限に抑えて作業できた点が良かったです。 既存のコードにはnullを適切に処理している部分もありましたが、ほとんどの箇所でnull処理が不十分であり、全体的にnullが入りやすい設計になっていたことを再認識しました。 おそらく、Flutter2でもnullチェックを意識してコードを書くことは大切だと思うので、そもそも既存のコードの書き方にも問題がありそうだなと感じました。 まとめ 今回はFlutter3でのNull Safety対応についてまとめました。 初めての移行作業ではありましたが、段階分けとエラーパターンの分類を行うことで、効率的かつ統一感のある対応ができました。 個々のエラーは一見シンプルではあるものの、パターン化して整理することで、どのように対応すべきか迷うことが少なくなり、結果的に作業がスムーズに進んだと感じています。 Null Safetyの対応は、手間がかかるものの、コードの信頼性や堅牢性が向上し、バグの予防に大きく寄与します。今回の記事が、Null Safety対応やFlutterのバージョンアップを迷っている方にとって、少しでも参考になれば幸いです。 ご覧いただきありがとうございました。
アバター
エブリーは2024年10月19日(土)に開催された Vue Fes Japan 2024 にゴールドスポンサーとして協賛させていただきました。 今回は参加レポートとして、会場の様子やセッションの感想についてお届けします。 イベント概要 Vue Fes Japanは、日本最大のVue.jsカンファレンスです。今年も多くの開発者が集まり、最新のVue.js関連技術や事例について学び、交流する機会となりました。 セッションの感想 1. キーノート Evan You氏によるキーノートセッションでは、Vue.jsエコシステムの最新動向と将来の展望について、深い洞察が共有されました。 主な注目ポイントは以下の通りです: Vueフレームワークの最新進展 Nuxt DevToolsの将来像 Viteビルドツールの現状と今後の方向性 現行のesbuild・Rollup・SWC構成から、RolldownとOxcへの移行戦略 OxcコンパイラとRolldownバンドラーの性能評価 これらのトピックを中心に、多岐にわたる内容が網羅されました。 個人的に、Evan You 氏が最近設立した Void Zero Inc. に非常に注目しています。 JavaScriptエコシステム全体のために、オープンソースかつ高性能な統合開発ツールチェーンの構築の実現によって、Vue.jsはもとより、JavaScript開発全般にもたらす可能性に大きな期待を寄せています。 2. Vue.js / Nuxt ハンズオン Vue Fes Japan では、毎年恒例のハンズオン企画として、Vue.js を学び始めたい方向けの教材を提供しています。今年は特別な取り組みがありました。 Nuxt の公式チュートリアル「Nuxt Tutorial」の作者である Anthony Fu 氏と Vue Fes Japan のコラボレーションにより、この公式チュートリアルの日本語版が先行公開されました。このチュートリアルがハンズオン企画の題材として使用されました。 内容は Vue.js の基礎(リアクティビティ、Composition API など)から始まり、Nuxt のコアなコンセプトまでが網羅されていました。 これから Vue.js・Nuxt を学び始めたい方には、このチュートリアルを通じて、より深い理解を得ることができると思います。 learn-nuxt.vuejs-jp.org 3. 次世代フロントエンドクロストーク 次世代フロントエンドクロストークセッションでは、JavaScriptエコシステムの最新動向と課題について活発な議論が展開されました。 主な注目ポイントは以下の通りです: フロントエンドビルドツールの進化 Viteが Vue や React SPA のデファクトスタンダードとして定着 Rust製ツール(Oxcコンパイラ、Rolldownバンドラーなど)の台頭 JavaScriptエコシステムのRust化の加速 AIによる大規模コード生成の可能性と課題 これらのトピックを通じて、フロントエンド開発の未来像について多角的な視点が提示されました。 特にRustの重要性が強調されたことで、私自身もRust学習への意欲が大いに刺激されました。このセッションを通じて、フロントエンド開発の将来がより鮮明に見えてきたと感じています。 スポンサーブース紹介 エブリーでは DELISH KITCHEN Web や DELISH KITCHEN チラシ などで Vue.js を採用しています。 いつも Vue コミュニティの恩恵を受けている我々もコミュニティのさらなる盛り上がりに貢献してくべく、スポンサーとして協賛させていただき、ブースも出展させていただきました! ブース エブリーでは、今回も弊社が提供するDELISH KITCHENのサービスをイメージしたブースの雰囲気を作りました。多くの方からDELISH KITCHENをを使っていますとの声をかけていただき、とても嬉しかったです。 ノベルティ 今回もDELISH KITCHENにちなんだノベルティを用意させていただきました。 ステッカー DELISH KITCHENグッズ CTOブレンドのコーヒーバッグ DELISH KITCHENグッズに関してはXフォローでの抽選プレゼントキャンペーンを行いました。DELISH KITCHENグッズに関してはたくさんの商品があるのですが、その中でも人気のある商品を中心に5つ準備させていただきました。参加者の方々にも好評で多くの方に参加していただけました! アンケート 今回、アンケートでは「Vue について教えて! 」と題して、「Vue の好きなところ」、「Vue の苦労したところ」について回答してもらいました。今回のアンケートでは付箋に自由に記述っしてもらう形式を取り、多くの方から様々な意見をいただくことができました。 回答いただいた多くの皆様、ありがとうございました! 各社スポンサーブースの様子 会場の1階にはスポンサーブースが展開され、各社の趣向を凝らしたブースに多くの人が足を止めていました。 どのブースも、それぞれの会社の特徴を生かした面白い展示が行われており、飽きることなく見て回ることができました。 GMO インターネットグループさんのブースでは、3種類の生成 AI を使って 天秤.AI by GMO の Web 画面を Vue.js で出力させた実装と実際の画面を展示して、好きな出力結果のアンケートを行っていました。 生成 AI の利用はとてもホットなトピックなので興味深かったです。筆者の好みは GPT-4 の出力結果でした! MedPeer さんのブースでは、「握力で技術的負債を粉砕しよう!」と題して、握力測定をすることでノベルティをもらえるという企画を行っていました。握力測定ができるブースは初めて見たのでとても新鮮でした。ちなみに、筆者は 43.6 kg という結果で、無事に学校で体力測定をしていた頃の過去の自分を超えることができました! まとめ Vue Fes Japan 2024 にゴールドスポンサーとして協賛できたことを光栄に思います。このイベントを通じて、Vue.js コミュニティの発展に寄与できたことは、私たちにとって大きな喜びです。 多くの方々にエブリーのブースにお立ち寄りいただき、Vue.js の最新トレンドやエブリーのサービスについて活発な議論を交わせたことに、心から感謝申し上げます。皆様との対話は、私たちにとっても大変刺激的で有意義な経験となりました。 今回のイベントでの経験を糧に、エブリーは今後も Vue.js コミュニティのさらなる発展に貢献していく所存です。Vue.js の最新情報やベストプラクティス、そしてエブリーのサービスを通じた実践的な知見を、継続的に発信してまいります。
アバター
はじめに エブリーでソフトウェアエンジニアをしている本丸です。 最近、Amazon Cognitoのユーザープールから別のユーザープールにユーザーを移行する方法について調査する機会がありました。 Amazon Cognitoに関しては色々な箇所で使われていると思いますが、ユーザーの移行について触れる機会はそれほど多くないかと思いますので紹介しようかと思います。 Amazon Cognitoとは Amazon Cognito(以降Cognitoと表記します)は、AWSが提供するウェブアプリとモバイルアプリ用のアイデンティティプラットフォームです。ユーザーの認証・承認を行うユーザープールとユーザーにAWSリソースへのアクセスを許可するアイデンティティプールを持っています。 DELISH KITCHENでは、ユーザーのメールアドレスの管理とメールアドレスを用いたサインインにCognitoのユーザープールを利用しています。 Lambdaトリガー CognitoにはLambdaトリガーという機能があり、ユーザープールに対してサインインなどのイベントが発生した時に、それをトリガーとしてLambdaを呼び出すことができます。 公式ドキュメントからの引用ですが、Lambdaトリガーとして設定できるイベントには下記のようなものがあります。 トリガーの種類 説明 認証前の Lambda トリガー サインインリクエストを承認または拒否するカスタム検証 サインアップ前の Lambda トリガー サインアップリクエストを承認または拒否するカスタム検証を実行する ユーザー移行の Lambda トリガー 既存のユーザーディレクトリからユーザープールにユーザーを移行する カスタムメッセージの Lambda トリガー メッセージの高度なカスタマイズとローカライズを実行する Cognitoのユーザープールへのインポート ユーザープールへのインポート・移行方法は2つ用意されています。CSVを用いた方法とLambdaトリガーを用いた方法です。 CSVを用いたインポート CSVを用いたインポートでは、指定されたフォーマットのCSVファイルを使用して一括でユーザーのインポートを行います。公式ドキュメントでは低労力で低コストなオプションとして紹介されていました。 こちらの方法では、セキュリティの観点からパスワードのインポートができないようになっています。そのため、移行の際にユーザー側でパスワードの変更が必要になります。 Lambdaトリガーを用いたインポート Lambdaトリガーを利用したインポートでは、前述したLambdaトリガーを起点にユーザーの移行を行います(上述の表の ユーザー移行の Lambda トリガー が今回説明するトリガーです)。このトリガーは、ユーザーがサインインする時とパスワードのリセットを行うときに発火します。 こちらの方法では、パスワードも連携されるのですが認証フローに USER_PASSWORD_AUTH または ADMIN_USER_PASSWORD_AUTH を指定し、ユーザー名とパスワードによる認証を行わなければならない点に注意です。 少しイメージしにくいかと思うので、次でもう少し詳細に説明します。 Lambdaトリガーを用いたユーザーの移行の実装 Lambdaトリガーを用いたユーザー移行の流れはおおよそ図のようになります。 ユーザーを移行したい先のアプリケーションでサインインもしくはパスワードリセットが呼び出されたのをトリガーにしてユーザー移行のLambda(図の user migration lambda )が呼び出されます。 ユーザー移行のLambdaの実装は次のようになります。 import { CognitoIdentityProviderClient, AdminInitiateAuthCommand, AdminGetUserCommand, CognitoIdentityProviderClientConfig, UserNotConfirmedException } from "@aws-sdk/client-cognito-identity-provider" ; import { UserMigrationTriggerHandler } from "aws-lambda" ; const userMigration: UserMigrationTriggerHandler = async ( event ) => { const config: CognitoIdentityProviderClientConfig = { region : 'ap-northeast-1' , } ; const client = new CognitoIdentityProviderClient(config); if (event.triggerSource == "UserMigration_Authentication" ) { const adminInitiateAuthCommand = new AdminInitiateAuthCommand( { UserPoolId : ${ USER_POOL_ID } , ClientId: $ { CLIENT_ID } , AuthFlow: "ADMIN_USER_PASSWORD_AUTH" , AuthParameters: { "USERNAME" : event.userName, "PASSWORD" : event.request. password , } , }); // 認証できるかチェック try { await client. send (adminInitiateAuthCommand) } catch (e) { console .log( `user auth failed: ${ e.message } ` ); throw e; } // cognitoに登録するユーザー情報構築 const adminGetUserCommand = new AdminGetUserCommand( { UserPoolId : process .env.DK_USER_POOL_ID, Username : event.userName, } ); try { const response = await client. send (adminGetUserCommand); // 移行先のユーザーに持たせたい情報を詰め込む event. response .userAttributes = { "email" : response.UserAttributes. find (( attr ) => attr.Name === "email" )?.Value ?? "" , "email_verified" : response.UserAttributes. find (( attr ) => attr.Name === "email_verified" )?.Value ?? "false" , } ; // 検証メールを送信しないため、下記を指定する event. response .messageAction = "SUPPRESS" ; event. response .finalUserStatus = "CONFIRMED" ; return event; } catch (e) { console .log( `get user failed: ${ e.message } ` ); throw e; } } return event; } ; このユーザー移行のLambdaの中で、移行元となるCognitoでユーザーの認証を行い、認証が成功した場合にユーザーの情報を取得します。その情報を移行先のCognitoに返すことでユーザーの移行を行います。 event.response に渡すデータを変更することで移行先のユーザーに持たせたい情報を変更したり、ユーザーがそのメールアドレスの正当な所有者であるかを確認するメールを送信するかなどを操作することができます。 まとめ Cognitoのユーザーの移行方法を調査して、ユーザーの移行を行うためにAWS公式で便利な機能が用意されていることを知ることができました。 2つの方法にそれぞれメリット・デメリットがあるかと思うので適切に使うようにしていきたいと思います。 参考資料 https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-import-users.html
アバター
こんにちは、DevEnableグループの 羽馬 です。 いよいよ日本最大のVue.js開発者カンファレンス「Vue Fes Japan 2024」の開催が目前に迫りました! 10月19日に開催されるこのイベントは、Vue.jsエコシステムの最新トレンドや先端技術が一堂に会する、開発者必見の祭典です。 エブリーは今年初めてゴールドスポンサーとして協賛し、Vue.jsコミュニティの皆様との交流を心待ちにしています。 この記事では、エブリーのブース内容の詳細や、カンファレンスの前後に開催するVue Fes Japan 2024関連イベントについてご紹介します。Vue.jsに興味がある方、エブリーの技術や文化に触れてみたい方は、ぜひ最後までお読みください! エブリーのブース紹介 エブリーのブースでは、開発組織やサービスの魅力を存分に感じていただけるよう、様々な工夫を凝らしています。 魅力的なノベルティ DELISH KITCHEN のオリジナルキッチングッズ 先着限定オリジナルタンブラー CTOオリジナルブレンドコーヒーバッグ 特別企画:every CTO Blend 今回特に注目いただきたいのが、オリジナルでブレンドしたコーヒー「every CTO Blend」のコーヒーバッグです。 CTOが厳選したブレンド豆使用 香り高い味わいをそのままコーヒーバッグに コーヒーバッグ制作の裏話や想いは、以下の記事で詳しくご紹介しています。ぜひご覧になってください! 人々へ明るい変化を提供する、オリジナルブレンドコーヒー「every CTO Blend」を制作 エブリーのブースにはお気軽にお立ち寄りください。技術の話題はもちろん、エブリーの魅力や開発文化について、私たちスタッフが熱意を持ってお伝えします。皆様とお会いできることを心より楽しみにしています。 Vue Fes Japan 2024関連イベントのご案内 Vue Fes Japan 2024の前後に、協賛企業の皆様と共催して特別イベントを開催します。これらのイベントは、Vue.jsコミュニティの交流をさらに深める絶好の機会です。 Vue Fes Japan 2024 Pre LT Party Vue Fes Japan 2024 After Night 両イベントとも公募LT枠をご用意していますので、ぜひご参加ください! さいごに Vue Fes Japan 2024は、Vue.js開発者コミュニティにとって貴重な学びと交流の場です。エブリーは、このイベントを通じて日本のVue.js開発の発展に貢献できることを光栄に思います。 Vue.jsに関心のある方、エブリーの技術や文化に興味をお持ちの方、ぜひVue Fes Japan 2024にご参加ください。エブリーのブースでお会いできることを心よりお待ちしています!
アバター
目次 はじめに 自動生成しようと考えた背景 トモニテが公開しているLPについて 既存のLP実装の課題 生成AIによるLP実装の自動生成 LP自動生成のためのアプローチ 実際に自動生成してみて OpenWebUI を利用した場合 LlamaIndex.TS を利用した場合 そのほかのアプローチ まとめ はじめに こんにちは。 トモニテ開発部ソフトウェアエンジニア兼、CTO室Dev Enableグループの庄司( ktanonymous )です。 今回の記事では、生成AIを利用してトモニテが公開しているLPの自動生成に挑戦してみた時の話をしたいと思います。 (※2024年7月下旬〜8月上旬時点での話になります。) 自動生成しようと考えた背景 まずはじめに、なぜLPの自動生成をしようと考えたのか、その背景を説明したいと思います。 トモニテが公開しているLPについて トモニテでは、企業様と提携したLPを複数公開しています。 LPの例 これらのLPは、TypeScript/React/Next.js を利用して実装されており、microCMS 1 を利用してコンテンツを管理しています。 各LPは静的ページとしてビルドされ、S3 にデプロイ、CloudFront を経由して配信されます。 また、各LPの仕様書は SpreadSheet/Figma にて管理されており、各LPの実装は仕様書を元に行われています。 基本的には、ビジネスサイドが仕様書を作成し、デザイナーがデザインを作成し、エンジニアが実装を行うというようなフローとなっています。 既存のLP実装の課題 全てのLPで共通しているパラメータや画像パーツなどのコンテンツは、microCMS を利用して設定できるようになっています。 また、各LPで利用するフォーマットやコンポーネント(プルダウン、選択パネルなど)は一定共通化されていますが、それぞれのLPに合わせて設問や表示方法などが異なっています。 そのため、新しいLPを作成する際には、既存のLPをコピーした後で仕様書を元に細かいチューニングを行う形で開発が行われています。 事業の拡大を目指す中で、LPの数も増えていき、LPの開発・運用に要する人的/時間的リソースの増加がジワジワと事業促進におけるボトルネックとして感じられるようになってきています。 そこで、エンジニアの開発工数やビジネスサイドの確認工数を削減し、事業のスピードアップを図るために生成AIを利用したLPの自動生成を検討しました。 生成AIによるLP実装の自動生成 LP自動生成のためのアプローチ 今回検討した生成AIによるLPの自動生成では、以下のようなフローをイメージしました。 既存のLPの仕様書および実装を AI モデルに embedding で学習させる。 新規LPの仕様書およびプロンプトを AI モデルに入力し、LPの実装を生成する。 生成AIを利用するにあたって、詳細なモデルを決定する以前に、ChatGPT 2 のようなオープンなモデルを利用するのかローカルLLMを利用するのかという観点があります。 ローカルLLMはモデルの性能が低めであったり実際に使用しているマシンのスペックが影響したりするため精度は落ちがちですが、今回は新しくコストをかけたくないという要望があったので、ローカルLLMを利用することにしました。 実際に自動生成してみて 実際にLPの自動生成を行ってみた時のことについて説明したいと思います。 ローカルLLMに関しては、様々なモデルをシンプルに利用することのできる Ollama 3 を採用しました。Ollama は、モデルの重みを量子化して推論を高速化することで CPU レベルのスペックでも LLM を扱えるようにした Llama.cpp 4 をラップしてローカルでも動かせるようにしてくれるツールです。 ちなみに、 Ollama で利用できるモデルは こちら から確認することができます。 実際に Ollama を組み込むために、以下の2つのパターンを試してみました。 Open WebUI 5 (ローカルで動かせる ChatGPT のような AI チャットツール) LlamaIndex.TS 6 (LLM を組み込んだアプリケーションを開発するための TypeScript 向けフレームワーク) OpenWebUI を利用した場合 Open WebUI + Ollama による LLM 開発環境を構築できるコンテナイメージが公開されているため、以下のコマンドで簡単にローカル環境を構築できます 7 。 $ docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama 上記のコマンドでコンテナを起動しておいてブラウザから http://localhost:3000 にアクセスするとOpen WebUI を利用した AI チャットツールが利用できます。 設定画面から Embedding のようなモデルの設定などが行えます。 Open WebUI のチャット画面( Open WebUI 公式サイト ) モデルやプロンプトに関しては色々と調整してみましたが、以下のプロンプトを llama3.1 に渡した時に一番良さそうなレスポンスが返ってきました。 プロンプト `ExistingLP.tsx` was created referencing `existing_lp_specification.csv`.\nLike that, create `NewLP.tsx` referencing `new_lp_specification.csv`.\nYou must show me full implementation. 生成された実装 import React, { useState, useEffect } from 'react' ; import { useForm } from 'react-hook-form' ; import { useRouter } from 'next/router' ; import EntryAPI from '../api/EntryAPI' ; export default function NewLP ( { campaign } : Props ) { const [ isError , ToggleError ] = useState( false ); const [ isSubmit , ToggleSubmit ] = useState( false ); const router = useRouter(); const methods = useForm( { shouldFocusError : false } ); const watch = methods. watch (); const entryAPI = new EntryAPI(); const onSubmit = ( data : DoNotUseAnyType ) => { ToggleSubmit( true ); const refererParams = getCommonEventParams(); const answers = { email : data.email, gender : data.gender, ... // レスポンスではコードが記述されていましたが省略しています // Additional fields from the CSV file venue : data.venue, experience : data.experience, } ; entryAPI.submitEntry(answers, refererParams) . then (( response ) => { console .log(response); router. push ( '/thank-you' ); } ) . catch (( error ) => { ToggleError( true ); console .error(error); } ); } ; return ( < div > { isSubmit ? ( 'Thank you for submitting your entry!' ) : ( < form onSubmit ={ methods . handleSubmit ( onSubmit ) } > ... // レスポンスではコードが記述されていましたが省略しています < label > Email: < input type = "email" name = "email" { ... methods . register ( 'email' ) } /> </ label > < label > Gender: < select name = "gender" { ... methods . register ( 'gender' ) } > { genders . map ( (gender ) => ( < option key ={ gender } value= { gender } > { gender } </ option > )) } </select> </ label > ... // レスポンスではコードが記述されていましたが省略しています <button type = "submit" >Submit</ button > </form> )} </div> ); } とりあえず画面を表示するくらいはできそうな実装が返ってきました。 実際のLPの実装や仕様書をお見せできないので伝わりにくいのですが、既存の実装を踏襲できていない部分や仕様書の内容を反映できていない部分が多くありました。 LlamaIndex.TS を利用した場合 次に、LlamaIndex.TS を利用してローカルで LLM を利用する方法を試してみました。 既存のLPがTypeScriptで実装されているので、組み込みやすいように TypeScript 向けのフレームワークを利用することにしました。 LlamaIndex.TS を利用して TypeScript で Ollama を動かす方法に関しては公式のチュートリアル 8 が参考になります(Node.js v18からしか対応していません 9 )。 ここでは、新しいページを作成して、そのページでLLMとのやりとりを行えるようにしようと考えました。 実際の画面や詳細は割愛しますが、このやり方でもあまり良い結果は得られませんでした。 新しいページでLLMを動かすための実装 結果が芳しくなかったので、クエリをページ上から入力できるようにすることまではしていません。 LLM とのチャットページの実装 ( src/pages/llm.tsx ) import { useState } from "react" ; const LpGenerator = () => { // ボタンをクリックするとmain関数が実行されるページ const [ query , setQuery ] = useState( "What did the author do in college?" ); const [ result , setResult ] = useState( "" ); const onClick = async ( e : any ) => { e. preventDefault (); const response = await fetch ( '/api/query_llm' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , body : JSON . stringify ( { query } ), } ); const data = await response.json(); setResult(data. result ); console .log(result); } return ( <> < p >LP Generator </ p > < button onClick = {onClick} >Run LLM Query</ button > { result && < p >{ result } </ p >} </> ) } ; export default LpGenerator; API ハンドラーの実装 ( src/pages/api/query_llm.ts ) import type { NextApiRequest, NextApiResponse } from 'next' ; import QueryLLMAPI from 'api/query_llm' ; export default async function handler ( req : NextApiRequest , res : NextApiResponse ) { const { query } = req. body ; const queryLLMAPI = new QueryLLMAPI(); const result = await queryLLMAPI.post(query); res. status ( 200 ).json( { result } ); } LLM とのやりとりを行う API の実装 ( src/api/query_llm.ts ) import fs from "fs/promises" ; import { Document , HuggingFaceEmbedding, Ollama, Settings, VectorStoreIndex } from 'llamaindex' ; interface IQueryLLMAPI { post ( query : string ): Promise < string > ; } export default class QueryLLMAPI implements IQueryLLMAPI { constructor () { Settings.llm = new Ollama( { model : "llama3.1:8b" } ); Settings.embedModel = new HuggingFaceEmbedding( { modelType : "BAAI/bge-small-en-v1.5" , quantized : false } ); } async post ( query : string ): Promise < string > { // テキストの読み込みと処理 console .log( "Reading text..." ); const path = "node_modules/llamaindex/examples/abramov.txt" ; const essay = await fs.readFile(path, "utf-8" ); // Documentの生成とインデックスの作成 console .log( "Creating document and index..." ); const document = new Document ( { text : essay, id_ : path } ); const index = await VectorStoreIndex.fromDocuments( [ document ] ); // クエリエンジンでクエリを実行 console .log( "Querying..." ); const queryEngine = index.asQueryEngine(); const response = await queryEngine.query( { query : query } ); // 結果をクライアントに返す return response. toString (); } } そのほかのアプローチ ローカル LLM である Ollama を利用する以外にも、GitHub copilot や OpenAI を利用したアプローチも検討しました(弊社では既に利用可能な状況だったため、一旦「新しい」コストは発生していないという体で考えていました)。 しかし、残念ながら、これでも工数改善に繋がるようなクオリティの生成結果を得ることはできませんでした。 まとめ 今回の記事では、生成AIを利用してトモニテが公開しているLPの自動生成に挑戦してみた時の話をしました。 結果としては、今回の検証では、生成AIを利用してLPの自動生成を行うことは難しいという結論に至りました。 しかし、仕様書のフォーマットや既存実装の渡し方、推論プロセスなど改善できそうな点はまだあると思っています。 大きめの実装を実際に運用できるようなクオリティで生成させることは簡単ではないかと思いますが、今後の AI 技術の発展も含めて、引き続き期待したい領域だと感じました。 LP運用に関しては、引き続き改善を進めていきたいと考えているので、進展があった時にはまた記事にできたらと思います。 最後まで読んでいただき、ありがとうございました。 microCMS ↩ ChatGPT ↩ Ollama ↩ Llama.cpp ↩ Open WebUI ↩ LlamaIndex.TS ↩ Open WebUI + Ollama 開発環境のセットアップ ↩ LlamaIndex.TS チュートリアル ↩ LlamaIndex.TS yarn パッケージ ↩
アバター
レシピメディアにおいて、たとえば検索数推移のような時系列的なデータを扱っていると、急激に検索数が伸びているワードを捕捉したいシーンがあります。 要因はものによって違いますが、これをSQLだけで完結してなるべく楽したい。が今回の目的です。 要するに異常検知をすることが目的なのですが、「上昇率がX%以上を検知する」ような単純なモデルではないが、ある程度統計的な根拠をもとに検知が可能で、かつPythonのライブラリをつかうほど複雑ではなくSQL上でわかりやすく書けることを主眼に置きます。 方針としては以下です。 特定ワードにおける検索数の時系列データが正規分布に従うと仮定する 特定ウィンドウにおける検索数の移動平均と、標準偏差を抽出し、有意水準を5%などとし、逸脱したものを異常値とみなす イメージはこんな感じです。各週ごとに適正な範囲を求めて、そこを逸脱した値を異常値としてみなします 本来は標準化すべきですが、簡単のため標準化処理は行わずに進めます。 と、いうことで早速やっていきます。異常検知までの全体的なステップは以下です。 特定ワードにおける週ごとの検索数を抽出する 4週間ごとの検索数の移動平均を抽出 4週間ごとの検索数の標準偏差(σ)を抽出 移動平均±2σを抽出 現在週の値を判定する まず今回想定するデータソースはこんな感じです。仮に search_data というテーブルに入っているとして進めて行きます。 date: 日付 query: 検索ワード num_of_searches: 検索数 まず特定ワード xxx の検索数を抽出します。今回は最終的に週ごとの移動平均を取りたいので、週ごとの集計にします。 なお実行環境はPrestoとします。適宜ご自身の扱っているSQLに読み替えてご確認ください。 ▼検索数の抽出 with weekly_data as ( select week(date) as week , sum(num_of_searches) as total_searches from search_data where q = 'xxx' group by week ) 次に移動平均と標準偏差を抽出します。ウィンドウ関数を使います。 ▼移動平均と標準偏差の抽出 stats as ( select week total_searches , avg (total_searches) over ( order by week rows between 4 preceding and 1 preceding row ) as moving_average , stddev (total_searches) over ( order by week rows between 4 preceding and 1 preceding row ) as moving_stddev from weekly_data ) 次に移動平均±2σを抽出します。先程のクエリで同時に計算してもいいですが、若干わかりづらくなるので、こちらで移動平均±2σを抽出します。これにより、異常値の範囲を明確にすることができます。 ▼移動平均±2σの抽出 , bounds as ( select week , total_seaches , moving_average , moving_stddev , moving_average + 2 * moving_stddev as upper_bound , moving_average - 2 * moving_stddev as lower_bound from stats ) これで準備ができました。過去4週間の移動平均±2σと今週の値を見比べてみましょう。現在週の検索数がこの範囲を超えているかどうかを確認することで、異常値を検知することができます。 ▼異常検知する select total_searches > upper_bound or total_searches < lower_bound as is_anomaly from bounds これで異常値を検出できます! 今回のクエリを実行することで、現在の検索数が過去のトレンドから逸脱しているかどうかを簡単に確認できます。今回はSQLでやりましたが、どちらかと言うとスプレッドシートのほうがより簡単にできそうな気もしてきました。スプレッドシートでは、関数を使って同様の計算を行うことができるため、視覚的にデータを確認しやすいという利点があります。 是非ご自身で試してみてください。データの異常検知は、ビジネスの意思決定において非常に重要な要素ですので、さまざまな手法を試してみることをお勧めします。
アバター