TECH PLAY

株式会社エブリー

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

410

はじめに この記事は every Tech Blog Advent Calendar 2024 の7日目の記事です。 エブリーでデータサイエンティストをしている山西です。 今回は、A/Bテスト結果のレポーティングを自動化した事例をご紹介します。 ビジネスサイドが抱く「統計学的なとっつきにくさ」を解消し、結果を解釈しやすく伝えるための試みです。 図1: 結果のレポーティングの雰囲気(評価指標に対して、ダッシュボード上で結果を確認できる) ※ 本記事はランダム化比較実験や統計的仮説検定の基礎知識を前提としています。これらの知見をビジネスに還元する取り組み事例として、何かしらご参考になれば幸いです。 以下、経緯を順に説明していきます。 背景 私たちが運営するレシピ動画サービス『DELISH KITCHEN』では、日々の機能改善に A/Bテスト基盤 ※1 を活用しています。 これは、 1. ユーザー展開の準備(control群、test群への割り当て) 2. 観察指標のデータ集計 3. 統計的仮説検定(観察指標の「test群とcontrol群の差」を検定) 4. 結果のダッシュボード可視化(BIツールRedashをインターフェースとし、日次バッチ更新) を一気通貫で行う仕組みです。 これまで数年にわたり活用実績を積み重ねており、現在では社内の複数事業部で利用されています。 アプリ内機能の開発・改善 機械学習アルゴリズムの性能検証 広告やランディングページのデザイン改善 など、その用途は多岐にわたります ※2 。 こうして、 ビジネスサイドがデータドリブンに仮説検証を試みる文化 が着実に根付いてきました。 ※1 A/Bテスト基盤の詳細については、以下の記事をご覧ください。 tech.every.tv ※2 参考までに、直近1年の実施回数は50回でした。A/Bテストの実験成熟度モデル:Fabijan et al. (2017)では、年間のテスト実施回数で成熟度を簡易的に見積もるアイデアが提唱されています。これにならえば、ちょうどWalk Phase(年に50回以下)からRun Phase(年に250回以下)の境界にあたり、大規模なA/Bテスト推進組織への道がひらけた状態ともいえます。 課題 一方で、 ビジネスサイドに結果を正しく解釈してもらうこと そのために適切な実験のデザインをすること に関しては、一定の課題感が残りました。 以下にその事例をAs-Is、To-Be ※3 の体裁で整理します。 As-Is(実際にあった例) To-Be(目指したい状態) ・有意差や信頼区間を考慮せず、指標の結果値だけで判断する ・誤差幅と有意性を考慮して結果を解釈できるようにしたい ・有意差が出ていないことを「効果が無かった」と断定してしまう ・有意差がない場合は「差があったとは言えない」と判断できるようにしたい ・有意差と効果量を混同し、「有意だからビジネスインパクトが大きいだろう」と解釈してしまう ・有意性と効果量を区別し、それぞれ正しく考察できるようにしたい ・「有意差が出ていないから、出るまで期間を伸ばそう」と判断してしまう(p-hacking) ・都合の良い結果を導く危険性を共有し、事前の実験デザインを遵守できるようにしたい ・結果を見ながら元の仮説を書き換える(HARKing) ・仮説が不明瞭なまま検証を進めようとする。 ・A/Bテストは仮説検証の手段であることを共有し、後から仮説を変える危険性を伝えたい ・大きな変更によるネガティブ影響を恐れ、展開率を必要以上に抑える ・結果を早く見たいので期間を短く設定する ・検出力を確保するため、適切なサンプルサイズと実験期間を設定できるようにしたい ※3 To-Beの部分が全く実践できていないわけではありませんが、共通認識として推し進める段階には至っていない現状をAs-Isと対比して示しています。 発生要因 前提知識のばらつき これらの問題の主な原因は、 結果を解釈する人々の前提知識にばらつきがあること だと考えられます。 統計的仮説検定の結果は本来、有意差や信頼区間の意味を理解しつつ、適切に解釈する必要があります。 しかし、専門知識を必ずしも有していない人々にその解釈を委ねると、「事実が示す以上の解釈」が生じる可能性があります。 その結果、「数字の一人歩き」や「データに基づかない意思決定」といった問題が発生しやすくなり、意思決定のリスクが増大してしまいます。 「ビジネスの関心事」と「統計的な正しさ」とのギャップ また、時には 統計的仮説検定としての理想的な実験デザインが完遂できない ことがあります。 先述した「サンプルサイズ不足の状態でA/Bテストを進めてしまう」ケースがその一例です。 ビジネスサイドは収益最大化のため、時には短期間でPDCAを回す判断を行いたい場合もあります。 一方、観察指標によってはサンプルサイズの確保に時間を要する場合があります。 そうなると、「サンプルサイズ確保のために数週間、数ヶ月かけて仮説検定の正しさを立証する」ことよりも、「1施策を1〜2週間で実施し、不確実性を認めつつ結果を判断したい」ことに興味が向く場合もあります。 これはこれで一つの尊重すべき視点である ※4 一方、統計的視点を薄め、感覚と経験則に頼る傾向を強めてしまうことになります。 これでは、A/Bテストの意義が薄れてしまいます。 ※4「1施策の結果考察の確からしさを犠牲にする」策が本当にKPIの最大化に寄与するか否かは、別途定量的に分析してみないとわからないことだと思います。が、本記事の範疇を越えるため、ここでの深入りは避けます。 それをサポートするのがデータサイエンティストの役割では? 「こうした問題を防ぐためには、データサイエンティストがサポートすべきでは?」という指摘はもっともです。 しかし、実際の運用においては、いくつかの課題が浮き彫りになっています。 運用規模の拡大 A/Bテスト基盤の導入初期は、データサイエンティストとビジネスサイドが密に連携して結果を解釈していました。 しかし、運用規模が多くの部署に拡大するにつれ、データチームが全施策に関与することが難しくなっています。 データ解釈の視点の啓蒙活動の限界 ビジネスサイドへデータ解釈の際の心構えを啓蒙することも有効な解決策ですが、それだけでは限界があります。 学習を促す側・される側双方に一定のコストがかかるうえ、個々人の学習意欲や、担当者交代による知識の断絶といった属人性の課題があります。 全社的な見解の統一の必要性 実務者間で解釈を共有しても、他の利害関係者がダッシュボードを見た際に、「数字の一人歩き」や「誤解」が再燃することがあります。 特に、これが意思決定の上層部との間で起こると、認識のズレが意思決定を揺るがす原因になり得ます。 課題解決のための方向性 ここまで挙げてきたように、「誰でも気軽にA/Bテストを推進し、結果をダッシュボードで観察できること」の弊害が見え始めました。 一方で、「データドリブンな仮説検証を全社的に試みようとする文化の良い点」は引き続き維持したいところです。 また、ビジネスのスピード感を優先するがために「科学的な正しさ」の比重を下げなければいけない場合も、「その不確実性によって起き得るリスク」を意思決定者が認知し、公平に判断してもらう状態を目指したいです。 このような経緯から、「統計的仮説検定のデータ解釈をもっと良い感じに共通認識化させたい」という機運が高まることとなりました。 解決策: ダッシュボードからレポーティングへの昇華 これらの課題間の解決策として「言葉で解釈を手助けする」レポートをダッシュボードに追加することにしました。 コンセプトは「記述的なダッシュボードから、言葉によるレポーティングへの昇華」です。 これまでビジネスサイドとA/Bテストの結果を振り返るやりとりの中で「事実の整理としてのレポートはある程度パターン化できる」という気づきから、実装する運びとなりました。 以下に、結果の説明文の生成イメージを紹介していきます。 有意性の有無 や、 観測値(testとcontrolの指標の差)のプラス、マイナス に応じて、動的に生成内容を切り替えるようにしています ※5 。 ※5: 今回の主題ではないため詳しくは触れませんが、Redash上でPythonを実行する機構を用いて、各種統計的検定結果を動的に取得、埋め込む形でレポートを構築しました。 例1: 有意に結果がプラスとなったケース 図2:有意に結果がプラスとなった場合のレポーティング 例2: 有意に結果がマイナスとなったケース 図3:有意に結果がマイナスとなった場合のレポーティング 例1、例2では、「有意性と実際の効果の量を区別し、それぞれ正しく考察できるようにしたい。」というTo-Beを意識しています。 例3: 有意差が観察されなかったケース 図4: 有意差が観察されなかった場合のレポーティング 「誤差幅と有意性を考慮して結果を解釈できるようにしたい」 「検出力を確保するため、適切なサンプルサイズと実験期間を設定できるようにしたい」 というTo-Beを踏まえた内容が含まれています。 こだわり ビジネスサイドにとって理解しやすい言葉を意識する(専門用語を過度に使用せず、統計独特の言い回しを適宜言い換える) 言外の解釈に発展させないようにする(「信頼区間を95%正しい」と誤認させない、「有意差がないことは、効果がなかったことを必ずしも意味しない」など) などの工夫と共に、慎重に言葉を選びました。 また、例3で挙げたように、理想的な実験デザインが完遂できなかったとしても、 その不確実性やリスクを事前に告知する 工夫を説明文に施しました。 意思決定者が、ビジネス視点とデータ解釈の視点を公平に判断できる状態を期待しています。 最後に A/Bテストの運用における実務での気づきから、「自動レポーティング」という新たなアプローチを開拓した事例をご紹介しました。 本記事執筆時点では、これから運用を始める段階です。 自動レポーティングの導入により、統計的な観点を伴う解釈を関係者間で共有し、データ解釈における視座の向上を期待しています。 今後も、データドリブンに施策推進を行う社内文化の醸成と、その質の向上を図っていきたいと考えています。
アバター
全社的にSSH辞めるためには この記事は every Tech Blog Advent Calendar 2024 の 6 日目の記事です。 はじめに エブリーTIMELINE開発部の内原です。 全社的にSSHの利用を中止することができたので、そのような意思決定をすることに至った経緯や、その後の状況について紹介します。 なお前提として、下記記事はAWSに限定した内容となっています。 エブリーではGCP(GCE)も一部のサービスで利用しているのですが、GCEについては下記で説明する問題の影響がなかったため対象外としています。 SSH利用を中止したい理由 以下のような理由から、運用的にいろいろ辛い部分があったためです。 脆弱性対応で疲弊する 一般的にSSHサーバとしてOpenSSHが用いられることが多いと思いますが、このソフトウェアには時折セキュリティ脆弱性の問題が見つかることがあります。この脆弱性については放置できないケースも多いので、その都度工数が発生します。 今年だと CVE-2024-6387 の問題がありました。 共有アカウントにおけるセキュリティリスク キーペアを用いてEC2にログインするケースなど共有アカウントでログインする運用では、退職者であってもログインできてしまうリスクがあります。 また共有アカウントの運用では、監査の観点でも誰がなにをしたかについても追跡が難しくなります。 個別アカウントでの運用は大変 かといって、ユーザ個別のアカウント運用を行うのはわりと面倒です。 手動で管理するのは当然として、なんらか外部サービスと連携してアカウント管理を自動化するアプローチであっても、面倒なことには変わりありません。 セキュリティグループ運用が面倒 SSHを使うためにはSSHポート番号(22番)を開放する必要がありますが、この管理方法についても考慮すべきことが多いです。 ポートは全体開放(0.0.0.0/0)するか? 全体開放しないならどういう運用で開放するか? 管理コンソールで担当者が直接更新するか? なんらかIaCツールを用いるか? IPアドレスが頻繁に変わる場合はどうするか? SSH利用を中止した後の代替手段 上記のようにSSHを利用し続けることは無視できないリスクがあると考えたため、SSHの利用を全社的に中止することにしました。 ただそうは言っても、現状の運用でSSHを利用しているケースも存在していたため、代替手段を用意する必要がありました。 EC2へのログインにSession Managerを利用する AWS Systems Manager Session Managerを利用することでSSHの代替を行うことができます。 Session ManagerはEC2インスタンスに対してSSHの代替となるリモートシェルを提供するサービスです。 最近のEC2インスタンスならば通常SSM Agentは起動していますが、数年以上前に作成したインスタンスの場合はSSM Agentが起動していないことがあるため、その場合はSSM Agentを 手動でインストール する必要があります。 また、インスタンスIAMロールには AmazonSSMManagedInstanceCore ポリシーがアタッチされている必要があります。 EC2インスタンスを利用しないアプローチ もしくは、踏み台用のEC2インスタンスを用いるのではなく、ECS Fargate Taskを都度起動するアプローチを採ることも可能です。以前にその対応を行った記事がありますので、参考にしてください。 RDS踏み台サーバをよく見かけるECS Fargate+PortForward+Adhocな機構に変更する 実際のSSH利用例と代替手段 EC2インスタンスへのログインを行なっているケース EC2インスタンスにログインしてなんらかシェル操作を行なっているようなケースです。その場合は以下のようなコマンドでリモートシェルを利用することができます。 対応前 $ ssh -i path/to/key.pem $ec2_user @ $ec2_host sh-5. 2 $ 対応後 $ aws --profile $profile ssm start-session --target $instance_id Starting session with SessionId: foo.bar@nrcazkfv3a6gkcmdmihy7i4pbq sh-5. 2 $ ローカル環境からのRDSへの接続用Proxyとして利用しているケース RDSのインスタンスはVPC内に存在するため直接接続することができないので、SSH Port Forwardingを利用してリモート接続するようなケースです。 例えば以下のようなコマンドでローカル環境からmysqlサーバに接続することができます。 対応前 $ ssh -L 3306: $remote_db_host :3306 $ec2_user @ $ec2_host $ mysql -h 127 . 0 . 0 . 1 -u $db_user -p $db_name Enter password: mysql > 対応後 このようなケースについても、AWS Systems Manager Session Managerのポート転送を利用することで代替することができます。 $ aws --profile $profile ssm start-session --target $instance_id \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters ' {"host":["YOUR-REMOTE-DB-HOST"],"portNumber":["3306"],"localPortNumber":["3306"]} ' $ mysql -h 127 . 0 . 0 . 1 -u $db_user -p $db_name Enter password: mysql > 全社の状況把握と方針策定 状況把握 まずは全社で利用している全EC2インスタンスのリストを作成し、それぞれのインスタンス利用状況を可視化することにしました。 その際は以下のようなコマンドで一覧化したものをスプレッドシートに書き出し、担当部署を割り当てて部署ごとに利用状況を記載してもらいました。 $ aws ec2 describe-instances | jq -r ' .Reservations[] as $r | $r.Instances[] | select(.State.Name!="terminated") | [$r.OwnerId, .InstanceId, (.Tags // [] | from_entries.Name // "NoName"), (.SecurityGroups[0].GroupName // "NoName"), .LaunchTime, .InstanceType, .State.Name] | @tsv ' 上記コマンドによって以下のような出力を得られます。 AccountName OwnerId InstanceId TagName SecurityGroupName LaunchTime InstanceType State.Name ************ i-***************** INSTANCE-NAME SECURITY-NAME YYYY-MM-DDThh:mm:ss+00:00 INSTANCE-TYPE running 対象としたインスタンス数は全社で70個ほどで、これを担当する複数の部署に割り当てました。 なお、部署によってはインフラ構成が大きく異なっているケースもあり、管轄する個数にはだいぶ偏りがある状態でした。(ちなみに自分が所属しているTIMELINE開発部では該当するインスタンスは存在しませんでした) 方針策定 各部署では以下のいずれかの方針を選択してもらうことにしました。 インスタンスの削除 stopping状態のままになっているインスタンスやすでに利用していないインスタンスなど、削除しても問題ないインスタンスについては削除することにします。 SSHポート番号閉鎖 本来はSSHサーバ自体を停止するのが望ましいのですが、EC2の機構上動作しているインスタンスからSSHを無効化するのが難しかったため、SSHポート番号の閉鎖で対応することにしました。 ポート番号が塞がれていれば事実上外部からSSHで攻撃されるリスクは考慮しなくてよくなると考えたためです。 その後の状況 最初にリストを作成してから1ヶ月半ほどで、全部署での対応が完了しました。 前述の通り部署によって対象個数に偏りがあったため最終的にはそれなりの時間がかかることになりましたが、各部署のご協力あって無事完了させることができました。 上記対応を行った結果、現在は全社的にSSHの利用が中止され、セキュリティ上のリスクは大幅に軽減されました。 また、今後新たにEC2インスタンスを起動する場合にも同様の対処が行われるよう、全社的な運用ルールを別途定める予定です。 まとめ SSHの利用を中止することで、セキュリティ上のリスクを軽減することができました。またSSHのアカウント管理に関する煩雑さもなくなり、運用コストの削減にもつながりました。さらに運用ルールを定めて、今後ともにセキュリティを維持していくことが重要と考えています。 以上、全社的にSSHの利用を中止するために行った取り組みについて紹介しました。
アバター
この記事は every Tech Blog Advent Calendar 2024 5 日目の記事です。 はじめに こんにちは、DELISH KITCHEN 開発部でソフトウェアエンジニアをしている24新卒の新谷です。 今回は12/8開催のISUCON14に向けて、ISUCON初参加の私が勉強したことについてまとめていきます。 また、everyはISUポンサーとして協賛しており、詳しくは以下をご覧ください。 tech.every.tv 初参加に向けたざっくりの戦略 今回参加したチームは、日本CTO協会の新卒合同研修で知り合った新卒メンバーで出場しました。 (日本CTO協会の新卒合同研修についてのブログは こちら ) 全員がISUCON初参加ということで、それぞれ役割を決めて、それに向けて勉強を進めました。 そのうち私は、DB周りのインデックス担当ということで、DBのインデックスの張り方を中心に勉強しました。 また、役割はあるもののチーム全員で共通して勉強したこととして、以下があります。 Go言語 N+1の対策 オンメモリキャッシュのやり方 JOINなどのSQL構文をスラスラ読める&書けるようにする 過去問を解く 特にN+1の解消に関しては、ISUCONでは頻出するパターンのため、JOINして解決するのかIN句で解決するのかキャッシュで回避するのかなど、事前にかなり話し合いました。 それぞれ詳しく勉強したことについては、以下で紹介していきます。 DBのインデックスについて なぜインデックスの勉強が必要か インデックス・ショットガンと呼ばれるアンチパターンがあるように、無闇にインデックスを張るとパフォーマンスが悪化することがあります。 特に、INSERTやUPDATEが多いテーブルは、書き込みのオーバーヘッドが大きくなるため、インデックスを張る際には注意が必要です。 MySQLのインデックス ISUCONでは、DBにMySQLを使用することが多いため、MySQLのインデックスについて勉強しました。 以下の記事は、インデックスの基礎を学ぶのに参考になりました。 こちらはInnoDBにおけるインデックスの基礎知識を学べる他、インデックスを張るときのよくある間違いについても解説されています。 techlife.cookpad.com こちらは、MySQLのクエリーライフサイクルやUsing filesort, Using whereが何をしているのかをトランプを例に解説されています。 www.slideshare.net 上記を勉強するとEXPLAINの結果の意味がわかるようになるのと、インデックスを張るときの注意点がわかるようになります。 Go言語 こちらに関しては私は普段から業務でGoを書いているので特段勉強はしませんでした。 ただ、ISUCONではDBを操作する際に sqlx を使うことが多いため、sqlxの使い方については事前に勉強しました。 全てのメソッドは覚えませんでしたが、以下についてはスラスラ書けるようにしました。 1行を取得するときの Get if err := db.Get(&user, "SELECT * FROM users WHERE id = ?" , id); err != nil { return err } 複数行を取得するときの Select if err := db.Select(&users, "SELECT * FROM users WHERE age = ?" , age); err != nil { return err } In句などを使うときの In query, args, err := sqlx.In( "SELECT * FROM users WHERE id IN (?)" , ids) if err != nil { return err } query = db.Rebind(query) if err := db.Select(&users, query, args...); err != nil { return err } Bulk Insertもできる NamedExec _, err := db.NamedExec( "INSERT INTO users (name, age) VALUES (:name, :age)" , users) if err != nil { return err } N+1の対策 N+1に関しては、大きく分けて以下の解決方法があると考えています。 JOINしてN個のクエリを1つにまとめる IN句を使用してN個のクエリを1つにまとめる クエリで取得しているデータをキャッシュする 基本的にJOINをする方が効率的ですが、実装が大変です。また、キャッシュは実装は簡単ですが、書き込みや更新があるデータに関しては注意が必要です。 そこで私たちのチームでは以下の方針で決めていました。 基本的にはJOINを使う方針 書き込みや更新がないデータは、キャッシュで実装する 1対多の関係にあるデータは、IN句を使う 書き込みや更新があるが、ユースケース的に書き込み処理などが少ないデータは、キャッシュで実装する また、上記以外に、アプリケーション側でデータを絞っているにも関わらず、LIMIT句をつけていない場合は優先してLIMIT句をつけることも重要です。N+1自体の解消にはなりませんが、これによってDB負荷のボトルネックが改善され、点数が伸びることもあります。 オンメモリキャッシュのやり方 N+1の解消などにおいて、オンメモリキャッシュを使う場合は、どのように実装するのかチームで決めていました。 まず、書き込みや更新がないデータに関してはMap型で事前にキャッシュするようにしていました。 ただ、書き込みや更新があるデータに関しては、スレッドセーフなキャッシュを実現する必要があります。 そこで、私たちのチームでは、ISUCON用に開発された catatsuy/cache を使うことにしました。 github.com 当初は、 sync のMapを使うことを検討していましたが、以下の理由からcacheを使うことにしました。 Genericsを使っているため、型キャストが不要 キャッシュの有効期限を簡単に設定できる パフォーマンスもSync.Mapとほぼ変わらない 他にも理由はありましたが、主に上記の理由からcacheを使うことにしました。 JOINなどのSQL構文をスラスラ読める&書けるようにする N+1の解消において、基本的にはJOINを使う方針となったので、チーム全員がJOINに対して慣れる必要がありました。 また、そもそもISUCONではサブクエリを使った複雑なクエリやORDER BY句を使ったクエリなども出題されるため、SQLをスラスラ読めるようにすることが重要だと考えました。 SQLは以下のネットの問題集を使って勉強しましたが、他にも問題集などはあるため正直なんでもいいと思います。 SQL練習問題 | TECH PROjin 普段ORMを使っていたりすると、意識してSQLを書かなかったりすることもあると思うので、良い勉強になりました。 過去問を解く 過去問に関しては、時間的に全て解くことは難しかったため、直近の問題を何度も解くことにしました。 ISUCON13 ISUCON12の予選 ISUCON11予選 private-isu(過去問ではないが) 解説などを見ながら解いたりして、ボトルネックの特定のやり方や、どのようなアプローチで解いているのかを理解しました。 また練習中は、Copilotを切ったりコピペをしないようにしていましたが、これが意外と練習になりました。 まとめ 以上がISUCON14に向けて勉強したことです。 本番どうなるかは分かりませんが、練習してきたことを活かして、全力で取り組みたいと思います。 また、DBなどはISUCONのために勉強しましたが、普段の業務でも使える知識が多かったです。 そのため、ISUCONに活かすだけでなく、普段の業務でも活かせる部分は積極的に取り入れていきたいと思います。
アバター
はじめに この記事は every Tech Blog Advent Calendar 2024 の4日目の記事です。 DelishKitchenやヘルシカのバックエンドやらインフラやらをやっているyoshikenです。 今回は弊社でも利用しているUID生成に便利なSonyflakeについて説明していきます。 UIDとUUIDの違い まず、UIDとUUIDの違いについて理解をしましょう。 UUID RFC 9562 で標準化されている"普遍的にユニークな識別子"のことです。UUIDは、主にデータベースの主キーや分散システムにおけるオブジェクト識別子として使用され、形式は以下のようになっています。 例: f81d4fae-7dec-11d0-a765-00a0c91e6bf6 (8-4-4-4-12 計32文字16進数というフォーマットです。細かい仕様はRFCを参照してください。) 注: RFC 4122は既に廃止されています。 【RFC 9562】新しい UUID の概要紹介 | ymstmsys site UUIDの目的は、 グローバル規模での一意性の担保 です。 UID こちらは特に定まったフォーマットなどはなく、 特定のスコープ内で 一意に識別が可能というのが目的です。 DBのAuto Incrementもそういう意味ではUIDと呼べます。 ソフトウェアの世界に限らず、社員番号や学校のクラスの出席番号もある意味UIDと呼んで差し支えないかと思います。 ここでの注意点はUUIDの目的であった グローバル規模での という点は保証していないということです。 たとえば僕のeveryでの社員番号が200番だったとしても、他社で社員番号が200番の人は別の人を指しています。 DBのAuto Incrementも別テーブルでは衝突をしてしまいます。 そういったグローバル規模での一意性を担保するのであればUUIDを使用すべきです。 とはいえ、例えば学校のクラス40人程度にUUIDで一意性を〜というのはオーバーエンジニアリングになってしまうので、UIDで出席番号を割り振る程度がコストも掛からず可読性もよくなります。 クラスや学年が変わると被ってしまうので、スコープを学内にして学籍番号にするなど、適切にコントロールをしていくことでUIDだけでも問題ない場合があります。 卒業式に「6年2組、出席番号番号f81d4fae-7dec-11d0-a765-00a0c91e6bf6、吉田健太」なんて聞きたくないですね。 Sonyflake sony/sonyflake: A distributed unique ID generator inspired by Twitter's Snowflake Sonyflake is a distributed unique ID generator inspired by Twitter's Snowflake. Sonyflake focuses on lifetime and performance on many host/core environment. READMEにも書いてあるとおり、Twitter社の Snowflake をインスパイアした分散型UID生成ライブラリです 構成要素は以下の通りで、合計63ビットで表現されます。 0 15 32 (ビット) +--------------------------+-----------------------------+ | タイムスタンプ (39ビット) | +--------------------------+-----------------------------+ |タイムスタンプ(続き)| シーケンス (8ビット)|マシンID (16ビット)| +------------------------------------------------------+ 実際に出力されるのは以下のような数列が生成されます。 542479593760621806 以下は生成するexampleコードです。 package main import "github.com/sony/sonyflake" func main() { instance := sonyflake.NewSonyflake(sonyflake.Settings{}) if instance == nil { panic ( "sonyflake not created" ) } id, err := instance.NextID() if err != nil { panic ( "ID not created" ) } println (id) } 弊社ではレシピのナンバリングやユーザーID発行に使用されています。 選定理由ですが、随分前なので正確なところが不明ですが、Sonyflakeが現行のUUIDv7と比べても UUIDv7が128bitにたいしてSonyflakeは63bitとサイズが小さく取り回しがよい Sonyflakeは全て数値かつ単調増加に近しい挙動なのでB-treeインデックスを考えると効率が良い UUIDv7もタイムスタンプがあり以前に比べるとパフォーマンスが良くなりましたが、それでもSonyflakに歩があります 生成コストが安い 見た目がわかりやすい(可読性) というメリットがあります。 そもそも導入当時の2016年前後ではUUIDv7はRFC化されておらず、ULIDも出始めギリギリといったところです。 またSonyflake自体分散システム利用されることが前提のため、オートスケールで複数台のノードが立ち上がっていても問題となりません。 以上のことから当時Sonyflakeを選定するのは妥当性があると思います。 また導入から現在も特に大きな障害は発生しておらず安定して運用できています。 まとめ Sonyflakeは、UUIDv4に比べて生成コストが低く、インデックスの効率も良いというメリットがあります。しかし、UUIDv7ほどのユニーク性は持っていないため、衝突が許されない要件での使用には適していません。 用法用量を正しく守り使用すれば高いパフォーマンスを発揮することが期待できます。 以上でSonyflakeの紹介を終わります
アバター
トモニテのウェブアクセシビリティ向上に向けて この記事は every Tech Blog Advent Calendar 2024 の 3 日目の記事です。 はじめに こんにちは!トモニテにて開発を行っている吉田です。 今回は最近私が少し気にするようにしている(今更?とは言わないでもらえると嬉しい...)ウェブアクセシビリティについて、所属しているトモニテを対象に記事にします。 そもそもアクセシビリティとは? 「アクセシビリティ」という言葉は、Access(近づく、アクセスするの意味)と Ability(能力、できることの意味)からできています。近づくことができる」「アクセスできる」という意味から派生して、「(製品やサービスを)利用できること、又はその到達度」という意味でも使われます。 Access + Ability -> Accessibility ウェブアクセシビリティは、ウェブにおけるアクセシビリティのことです。利用者の障害などの有無やその度合い、年齢や利用環境にかかわらず、あらゆる人々がウェブサイトで提供されている情報やサービスを利用できること、またその到達度を意味します。 なぜウェブアクセシビリティを意識する必要があるのか 現代社会でウェブサイトは老若男女が利用する重要な情報収集源の 1 つとなっています。 しかし、ウェブアクセシビリティに配慮して作られていないと利用者によっては情報を得ることが難しくなってしまいます。 そんな状況を防ぐためにウェブサイトで提供している情報やサービスを誰もが利用できるようにウェブアクセシビリティを確保する必要があります。 ウェブアクセシビリティを確保できているとは ウェブアクセシビリティを確保できているとは以下の状態を指します。 目が見えなくても情報が伝わること・操作できること。 キーボードだけで操作できること。 一部の色が区別できなくても得られる情報が欠けないこと。 音声コンテンツや動画コンテンツで、音声が聞こえなくても話している内容が分かること。 3 ウェブアクセシビリティが確保できている状態とは? より 上記を満たしたウェブサイトであれば視覚障害のある人、聴覚障害のある人、色覚特性のある人など、ウェブサイトの閲覧にお困りの症状をお持ちのかたでもウェブサイトを介して情報を入手したり、サービスを利用できたりするようになります。 そこでエンジニアとしてウェブアクセシビリティにどう貢献できるか考えてみました。 私が考えたのは以下 2 点が開発業務において関わってくることではないかと考えました。 目が見えなくても情報が伝わること・操作できること。 キーボードだけで操作できること。 (ここまではウェブアクセシビリティとは? 分かりやすくゼロから解説!を参考に記載) https://www.gov-online.go.jp/useful/article/202310/2.html 記事では 1 つ目の「目が見えなくても情報が伝わること・操作できること」に焦点を当てます。 スクリーンリーダーを使ってトモニテ web をさわってみた 今回は目が見えない人やロービジョンの人を想定してスクリーンリーダーを使ってトモニテ web を使ってみました。 スクリーンリーダーに利用したのは Mac に標準搭載の VoiceOver です。 スクリーンリーダーを利用してみて気になったのが alt 属性の設定漏れです。 alt 属性は周知の通り <img> 要素で指定された画像が読み込まれない場合に表示する予備(代替)テキストを指定します。 それだけでなく alt テキストはスクリーンリーダーや他の支援技術によって使用され、音読されたり、点字出力端末に送られたりコンテンツを十分に活用できるようサポートする役割があります。 mdn にも alt 属性の指定には以下のような記述がありました。 画像の alt 文字列を選ぶときは、ページ上に画像があることに触れずに、電話で誰かにページを読み聞かせるときのことを想像してみてください。 HTMLImageElement: alt プロパティ より では 実際に存在した alt 属性の設定漏れについてふれていきます。 こちらはトモニテのアプリストアへのリンク画像です。 スクリーンショット内、下部の四角い箱内のテキストは Voice Over で読み上げられるテキストです。 スクリーンリーダーで読み込んでみるとリンクが設定されているが画像だということはわかりますが画像についての説明がありません。 ユーザーからしてみればそこに何かはあるのに内容がないとなっているのは不自然で、しかしその領域をタップするとストアに遷移するという状況です。 原因はシンプルで画像の alt が指定されていなかったことでした。 <img alt src='https://~~' alt 属性を props に渡す形で修正しました どの画像に alt 属性の設定漏れがあるのか特定したいのですが、スクリーンリーダーを使って調べるには数が膨大ですし、コード上で検索をかけるにしても全ページを対象に調べるのは少し骨が折れそうです... トモニテでは Next.js を利用しているのですが何か良い方法がないかと調べたところ eslint-plugin-jsx-a11y パッケージが有効だということが分かりました。 eslint-plugin-jsx-a11y 利用手順 eslint-plugin-jsx-a11y を利用するには前提として ESLint のインストールが必須になります。 # npm npm install eslint --save-dev # yarn yarn add eslint --dev eslint のインストールが完了したら eslint-plugin-jsx-a11y をインストールします。 # npm npm install eslint-plugin-jsx-a11y --save-dev # yarn yarn add eslint-plugin-jsx-a11y --dev インストールが完了したら.eslintrc.js の rules に'jsx-a11y/alt-text'を追加します。 'jsx-a11y/alt-text': error 「これで alt 設定漏れが検知できる!」とリンターを走らせてみたのですが何も検知できません...(alt が設定できていないコンポーネントがあるのは確認済) ドキュメントをよく見ると以下の記載がありました。 By default, this rule checks for alternative text on the following elements: <img> , <area> , <input type="image"> , and <object> . jsx-a11y/alt-text より そのため実装内に <img> 要素が存在しない場合、検知できません。 そのため alt-text ルールにオプションを加えることにしました。 'jsx-a11y/alt-text': [ 'error',{ 'img': ['componentA', 'componentB'] } ], これは img をラップしている componentA と componentB に Props として alt が渡っているか確認することができます。 改めてリンターを走らせると以下のようにエラーとして alt 属性の設定漏れを検知することができました! /app/src/pages/example.js 20:13 error componentA elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text まとめ 今回は alt 属性にのみ焦点を当てましたが、アクセシビリティを向上させるにはその他にも改善することはたくさんあります。 引き続きアクセシビリティを確保できるよう改善を進め、トモニテをよりたくさんの方に利用してもらえるサービスにしていきたいと思います! 参考資料 https://www.gov-online.go.jp/useful/article/202310/2.html developer.mozilla.org www.npmjs.com github.com
アバター
はじめに 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のバージョンアップを迷っている方にとって、少しでも参考になれば幸いです。 ご覧いただきありがとうございました。
アバター