TECH PLAY

タイミー

タイミー の技術ブログ

264

イベント概要 2023年8月24日にGaudiy社との共催で「 不確実性を乗りこなす強いプロダクトマネジメント組織のデザイン 」と題してプロダクトマネジメント組織に関する勉強会を開催しました。 不確実性の高いマーケットに対して、高い成果を出せるチームにしていきたいPdMやPOの方、組織設計からプロダクトのアウトカムを高めたいと考えているEMの方などに特にお勧めの勉強会でしたのでイベントの中から高石さんによる「『わからない』に立ち向かい続け成果を生み出す、実験志向なチームの育み方」の講演レポートをお送りします。 今回のスピーカー 「『わからない』に立ち向かい続け成果を生み出す、実験志向なチームの育み方」 不確実性の高いゴールに付き物の「わからない」 プロダクト開発ではゴールを設定します。タイミーではスクラムを採用しているチームが多いのでプロダクトゴールと表現されます。 RetentionRateの10%向上といった定量的なゴールもあれば、ユーザーストーリーの形式で特定属性のユーザーが○○を実現できるようになるといったゴールもあると思います。近年はこの様に何かしらのゴールに向かってイテレーティブな開発をすることが一般的になってきました。 ここで付き物なのが「わからない」ということです。ゴールが定まってもそこにどうやれば辿り着けるのかは「わからない」に満ちています。PdMやPOの方で「わからない」のフィードバックを貰ったことがない人はあまりいないのではないでしょうか? でもこの「わからない」ことってネガティブなことなのでしょうか? 人間にとって「わからない」ものは本能的に身構えてしまったり、気持ち悪さを感じることが多いと思います。一方でこの「わからない」気持ちは「わかりたい」という行動への原動力でもあります。「わからない」を放置しておくとモチベーションの低下に繋がってしまいますが、適切に解消が出来ればプロダクトの成果に繋がります。 「わからない」を「わかる」にする力がプロダクトの成果を生む 普段、仕事で強く意識することは少ないですが日常業務の中でも「わからない」ことが沢山あります。 こういった様々な疑問がある中で皆さんは無意識に解消する行動を取っていると思います。インタビューをしてみたり、A/Bテストを実施したり、言語化こそされていなくてもわかる様にするアクションを取っているのではないでしょうか。 その様な行動で「わからない」を一つずつ「わかる」に変えていくことで不確実性の高いゴールに対しても道筋が見えてきます。ゴールの達成方法がわかるようになってくるのでプロダクトの成功にまた一歩近づくことが出来ます。 逆に「わからない」を解決出来ないとBIGBANGリリースに繋がったり、出したは良いけどその後の計測が上手く出来ずに成功したかも失敗したかも判断出来ないといったことが発生します。それだけならまだしも「わからない」を解決出来ず、不確実性を許容出来ないチームになってしまうと簡単なゴールしか設定できなくなってしまうリスクもあります。 基本的にどのプロダクトも新規性が高くまだ世の中に無いものを生み出しているわけですから、文化として「わからない」を許容し、それを解消しつづける組織力をいかに創れるのかが企業の競争力に直結すると思います。 タイミーのケースから見る、チームの実験を支える環境づくり ユーザビリティテストやA/Bテストなどについてはよくまとまった書籍がたくさんありますし、皆さんも勉強されていることと思います。しかし、座学として知識を習得しても、それらを実際にチームとして行動に移すまでには辿り着けないことの方が多いのではないでしょうか? そこでタイミーでは「チームが実験を行ったり情報収集する、心理・物理のハードルを極限まで取り除く」ということを行っています。 チーム専属のアナリスト チーム専属になる前までは分析チームに依頼をして、回答を待つ形式でした。この場合、分析チームが忙しい場合だと答えが分かるまで1〜2週間掛かってきます。このようなことになると既にスプリントの1週間を超えてしまいます。依頼側としてもそんなに時間がかかるなら別のやり方を模索しよう、となり依頼することそのものを諦めてしまいがちです。 しかし、スクラムチームに1人専任でプロダクトアナリストが所属する様になったことで物事の大小を問わず、迅速な解決ができる様になり、定量面の仮説検証速度が向上しました。 ユーザーインタビューのハードルを極限まで落とす「 Interview as a Service 」 タイミーではユーザーと話したいと思ったらとにかくすぐに話せるようになっています。 Googleカレンダーにパブリックな仕組みがあってユーザーインタビューの予定を作ると自動で人がアサインされ、議事録が用意されて、インタビューアーのリクルーティングまで完結する仕組みがプロダクトマーケティング部によって運用されています。 このプロセスでは最短で明日、中央値でも2、3日後には望んだセグメントの人とお話が出来ます。 これによりスプリントの中に収まってくるので事前にリサーチして準備をしよう、ではなくスプリントの中で解くといったケースまで出てきたのも面白かったポイントです。 ここまで環境を整えていくと勝手に実験が増えていきます。実験をしよう、しようと思っているうちは中々実験が進みませんが、阻害する要因を全て取り除いていくと皆も実験したい気持ちはそもそもあるので自然と実験が増えます。 実験に特化したフレームワークの採用 『LeanUX』の中で紹介されているLeanUXCanvasを採用しています。 1〜8までのボックスがあるのですが、特に7が面白く「一番はじめに学習しないといけないこと」を明示しています。今回の言葉でいうと「わからないものの中でもっとも早くわかる様にしないといけないこと」ですね。「わからない」ことの存在を事前にフレームワークで宣言をしてその解消の重要性も説いていることがユニークに感じています。 取り組みのとある共通点 どのケースも全く違う取り組みに見えて、共通点があります。 ① ゴール達成の過程に生まれる「わからない」を「わかる」にする力をつけること ② チームを取り巻く、組織や環境面からアプローチしていること 「わからない」を爆速で楽にわかるようにするといった観点でチームや組織を組成すれば高い不確実性も許容して実験を重ねて解決できる強いチームが生まれます。 また「わからない」を解決するだけであれば手段はさまざまありますが、組織や環境面からのアプローチはレバレッジが効きます。例えばインタビューの話であれば開発チームに限らず、営業やマーケティングチームが使ったりしていて、全社に波及しています。 まとめ まとめると今日は不確実性という言葉は広いので「わからない」と置き換えましたが、この「わからない」ことをネガティブに受け取って欲しくないと思っています。「わからない」ことも当然ですが、すぐ「わかる」ことよりも「わからない」に対して挑戦して考えていく方が生産的でプロダクトの組織としてもより成果をあげれるのかなと思います。 実験や仮説検証でも個人だけの頑張りではなくチームとして取り組めることが多くあります。プロダクトのチームは元より実験をしたいので妨げる要因を取り除いていくことで自然と実験する文化が育まれ、不確実性に強いチームになっていきます。タイトルに繋がっていきますが皆で「わからない」に立ち向かい続け成果を生み出す、実験志向なチームを育てていきましょう。 充実のパネルディスカッションは本編動画から! www.youtube.com 実験文化のある組織をいいなと思った方は是非お話しましょう product-recruit.timee.co.jp
アバター
こんにちは! タイミーのデータアナリストの@takahideです。 今回は、メンバーが行っている「勉強会」を紹介させていただきます。 ご紹介する内容は以下になります。 ・勉強会ではどんなことをやっているの? ・勉強会を通じた業務の広がりとは? 勉強会について 勉強会の具体例 背景 勉強会で用いた本 勉強会の進め方 勉強会を通して 効果検証の講習会 まとめ We’re Hiring! 勉強会について まずは、勉強会に関して簡単に紹介させてください。 タイミーでは、有志のメンバーが勉強会を立ち上げる文化が根付いています。 その中でも、データアナリストの勉強会は大きく2種類あります。 一つは、「会計」「プロジェクトマネジメント」などテーマをもとにした「一話完結型」のもの。 もう一つは、課題本を決めて輪読する「読書会型」で、今回はこちらをご紹介します。 勉強会の例 「読書会型」の勉強会の一番の目的は「分析力の向上」です。 勉強会を通じて、分析手法に関する共通言語が浸透することで、分析スキルの底上げだけでなく、 分析プロセスや結果に関するコミュニケーションが円滑になりました。 また、他部署の方と勉強会を行うことで、複数部署間のコミュニケーションもスムーズになりました。 プロダクトチームと行なった勉強会の感想 今回は「効果検証」に関する勉強会を一つの事例として紹介できたらと思います。 勉強会の具体例 背景 効果検証の勉強会は、データアナリストの検証手法の習得を目的に始まりました。 タイミーの組織が大きくなるにつれて、検証が必要な施策の件数も増加傾向にあります。 また、マーケティング施策のように、ABテストでの検証が難しいものも増えており、手法の共通認識を作るニーズが高まっていました。 勉強会で用いた本 勉強会では、『効果検証入門〜正しい比較のための因果推論/計量経済学の基礎』(安井 翔太)を課題図書として用いました。 傾向スコア、差の差分法(DID)など効果検証の手法が網羅的に説明されているのと、コードが豊富なのが良かったです。 勉強会の進め方 勉強会は、週に一度、担当者がgoogle collabを用いて発表を行い、他メンバーから質疑応答を行いながら進めています。 コードを確認しながら進めることで理解が深まりやすくなっています。 勉強会を通して 大きく3つの活動に繋がりました。 一つ目は、当初の目的だった「効果検証手法に関する共通言語の促進」です。 特に、検証手法を用いる際の注意点を共通認識として持つことができました。 例えば、回帰不連続デザイン(RDD)を利用する際には、「分析対象が介入に関する状態を操作できない」ことが求められますが、こうした条件が満たされない場合は、逆に、集積分析(bunching analysis)のような操作を前提にデザインされた手法を試すなどです。 二つ目は、勉強会をきっかけに、データアナリスト内でpythonを推進する活動に繋がったことです。 勉強会はpythonで実行しましたが、勉強会の内容を踏まえてタイミーの実データを用いた検証を行う際に、pythonへの慣れの有無がボトルネックになっていることが分かりました。 そこで、現在は、pythonを通常の分析業務で自然に扱えるための学習パッケージの構築を進めています。 三つ目は、全社的な講習会に繋がったことです。 こちらに関して次に詳細をお伝えできたらと思います。 効果検証の講習会 冒頭でもお伝えしましたが、現在タイミーは組織拡大に伴い、効果検証のニーズが増加しています。 そこで、今回の勉強会の内容と、過去の分析ナレッジを踏まえて、全社的な効果検証の講習会を実施しました。 効果検証の目的から始まり、交絡因子など、効果検証で注意するべき内容をお伝えしました。 また、効果検証を進める際に、データアナリストと事業部がどのように連携すると良いかをお話しました。 まとめ データアナリストの勉強会のご紹介いかがだったでしょうか。 改めてにはなりますが、データアナリストが勉強会を行うメリットに「分析手法の習得」と「共通言語の促進」があると思います。 特に後者は、勉強会のナレッジを他部署の方に展開するなど、コミュニケーションの円滑化に繋がっていると考えます。 タイミーは事業、組織ともに拡大傾向にありまして、データアナリストが活躍する機会が多く存在しています。 これからもデータアナリスト発信で意思決定に役立つ情報を展開することで事業の成長に貢献したいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! カジュアル面談 も行っていますので、少しでも興味がありましたら、気軽にご連絡ください。
アバター
こんにちは☀️ タイミーでアナリストとアナリティクスエンジニアしてますokodoonです 今回の記事はdbt CloudでPull Requestを作るときに、レビュー負荷が高くなってしまっていた問題を解消できるように、コンパイル済みのSQLをPR上にコメントするような仕組みを作成したことについての紹介です。 もし同じような課題感を抱えている方がいらっしゃれば、参考にしていただければ幸いです 課題感 今回選択した解決策 背景/前提 実装概要 各ステップの説明 PRの情報をもとにprofiles.ymlの動的生成 コンパイル処理の実施 PR上にコメント どんなふうに動くかみてみる 結果 We’re Hiring! 課題感 弊社のデータ基盤ではDWH層DataMart層は「分析用に加工されたデータを扱う層」として定義しています。 各種ドメインに依存した集計や変換のロジックが含まれるため、この層のモデリングに関しては基盤開発側のレビューのみでなくアナリスト観点でのレビューも必須となります。 ですが、分析ドメイン観点でのレビューが必要な場合に、純粋なSQLではないdbtモデルがアナリストレビューの障壁になることが多いです。 またアナリスト以外であっても「このmacroがどのようにコンパイルされるか」を把握するのは少し面倒だったりします 今回選択した解決策 そこでdbtモデルをコンパイルしたSQLファイルをPullRequest上にコメントするような仕組みを考えました。 この実装によって先ほど挙げた課題感が以下図のように解決されることを期待して開発しました 背景/前提 1. 開発環境について 弊社ではdbtを活用したデータ基盤の開発を行っており、 dbtモデル開発をdbt Cloud上の統合環境にて実施するような流れになっています。 2. CIジョブについて CI用のdbt CloudのJobは RUN ON PULLREQUEST で実行されて、以下のようなコマンドが実行されています。 dbt build --select state:modified+ mainブランチとの差分があるdbtモデルがBigQuery上にビルドされている状態です(本来CIで走るdbtコマンドをレイヤーごとに分割していますが、ここでは簡略化しています) 3. CI環境のスキーマ名とcustom_schemas.yml戦略 dbt Cloudで設定できるCI Jobではtarget schema名の命名が dbt_cloud_pr_<job_id>_<pr_id> となっており、連携レポジトリのPR番号に対して動的に作成されます。 (DBT_JOB_SCHEMA_OVERRIDEでこの命名規則の上書きもできますが、PR単位でCIテーブルが作成されて欲しいので、デフォルトの命名規則にしたがっています。) 参考: https://docs.getdbt.com/docs/deploy/continuous-integration#how-ci-works また、弊社では開発環境とCI環境のスキーマ名が {{ターゲットスキーマ名}}_{{カスタムスキーマ名}} になるように custom_schemas.sql で定義してあります。 そのため、CI環境のテーブルは dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} の命名規則で作成されたデータセット名以下に作成されている状態です。 実装概要 作成したYAMLはこちら(クリックで展開) name : DBT Compile and Comment on PR on : pull_request : types : [ opened, synchronize, reopened ] jobs : dbt_compile : runs-on : ubuntu-latest permissions : contents : read pull-requests : write id-token : write env : DBT_ENVIRONMENT : dev steps : - name : Check out repository code uses : actions/checkout@v3 with : fetch-depth : 0 ref : ${{ github.event.pull_request.head.ref }} - name : Fetch base ref run : git fetch origin ${{ github.event.pull_request.base.ref }} - name : Set Up Auth uses : "google-github-actions/auth@v1" with : token_format : "access_token" workload_identity_provider : "hogehogehoge" service_account : "hogehogehoge@hogehoge.iam.gserviceaccount.com" - name : Set up Cloud SDK uses : google-github-actions/setup-gcloud@v1 - name : Set up Python uses : actions/setup-python@v3 with : python-version : 3.11 - name : Install dependencies run : | python -m pip install --upgrade pip pip install dbt-core dbt-bigquery - name : Generate profiles.yml run : | chmod +x ci/compile_sql_comment/generate_profile.sh ci/compile_sql_comment/generate_profile.sh ${{ github.event.pull_request.number }} - name : Compile DBT id : compile run : | dbt deps dbt compile --profiles-dir . --target dev --profile timee-dwh compiled_sqls="" files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.ref }} | grep '\.sql$' || true ) if [ -n "$files" ] ; then for file in $files; do compiled_file_path=$(find target/compiled -name $(basename $file)) echo "Compiled file path: $compiled_file_path" if [ -n "$compiled_file_path" ] ; then compiled_sql=$(cat "$compiled_file_path" ) compiled_sqls="${compiled_sqls}<details><summary>${file}</summary>\n\n\`\`\`\n${compiled_sql}\n\`\`\`\n\n</details>" fi done fi printf "%b" "$compiled_sqls" > compiled_sqls.txt - name : Comment on PR uses : actions/github-script@v6 with : script : | const fs = require('fs'); const output = fs.readFileSync('compiled_sqls.txt', 'utf8' ); const issue_number = context.payload.pull_request.number; const owner = context.repo.owner; const repo = context.repo.repo; async function processComments() { const comments = await github.rest.issues.listComments( { owner : owner, repo : repo, issue_number : issue_number, } ); const dbtComment = comments.data.find(comment => comment.body.startsWith('DBT Compile Result')); const body = ` DBT Compile Result : \n$ { output } `; if (dbtComment) { await github.rest.issues.updateComment( { owner : owner, repo : repo, comment_id : dbtComment.id, body : body } ); } else { await github.rest.issues.createComment( { owner : owner, repo : repo, issue_number : issue_number, body : body } ); } } processComments(); actionsの流れを説明するとこうなります。 PRブランチにチェックアウトして、PRブランチとmainの差分を確認するためにfetch workload_identity_providerを用いたBigQueryへの認証 必要パッケージのインストールと使用するパッケージの宣言 PRの情報をもとにprofiles.ymlの動的生成 コンパイル処理の実施 dbt compileの実行 mainとの差分があるファイルだけを抽出 差分ファイルのcompile結果を文字列化 PR上にcompile結果がなければdbt compile結果を新規コメント。既にcompile結果がコメントされていたらコメントをupdate 各ステップの説明 説明が必要そうなステップの説明をしていきます PRの情報をもとにprofiles.ymlの動的生成 上で述べた通り、CI環境のテーブルは dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} の命名規則で作成されたデータセットに配置されています。 dbt compileの出力結果のデータセット名が dbt_cloud_pr_<job_id>_<pr_id>_{{カスタムスキーマ名}} になるように、デフォルトスキーマ名を動的に宣言するためのprofiles.ymlを作成するシャルスクリプトを作成しております #!/bin/bash set -e cat << EOF > profiles.yml timee-ci-dwh: target: ci outputs: dev: type: bigquery method: oauth project: ci_env_project schema: dbt_cloud_pr_39703_ $1 execution_project: ci_env_project threads: 1 EOF 参考: https://docs.getdbt.com/docs/core/connect-data-platform/bigquery-setup#oauth-via-gcloud これによりdbt compileの出力結果が、このターゲットスキーマ名を参照したスキーマになります。 コンパイル処理の実施 run : | dbt deps dbt compile --profiles-dir . --target ci --profile timee-ci-dwh compiled_sqls="" files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.ref }} | grep '\.sql$' || true ) if [ -n "$files" ] ; then for file in $files; do compiled_file_path=$(find target/compiled -name $(basename $file)) echo "Compiled file path: $compiled_file_path" if [ -n "$compiled_file_path" ] ; then compiled_sql=$(cat "$compiled_file_path" ) compiled_sqls="${compiled_sqls}<details><summary>${file}</summary>\n\n\`\`\`\n${compiled_sql}\n\`\`\`\n\n</details>" fi done fi printf "%b" "$compiled_sqls" > compiled_sqls.txt dbt compileをPRブランチで実行 git diffで差分があった.sqlファイル名をdbt compileの実行結果であるcompile済みsqlファイルが格納される target/compiled/ 以下でfindを実行してfileパスを取得 fileパスのcat結果を折りたたみタグ内に格納して文字列に追加 って流れの処理にしています。 このような処理にすることでSQLコンパイル結果のPRコメントを必要分だけコメントできる形としました。また、折りたたみタグに格納することでPullRequestの可視性を損なわないようにしました。 PR上にコメント - name : Comment on PR uses : actions/github-script@v6 with : script : | const fs = require('fs'); const output = fs.readFileSync('compiled_sqls.txt', 'utf8' ); const issue_number = context.payload.pull_request.number; const owner = context.repo.owner; const repo = context.repo.repo; async function processComments() { const comments = await github.rest.issues.listComments( { owner : owner, repo : repo, issue_number : issue_number, } ); const dbtComment = comments.data.find(comment => comment.body.startsWith('DBT Compile Result')); const body = `DBT Compile Result:\n${output}`; if (dbtComment) { await github.rest.issues.updateComment( { owner : owner, repo : repo, comment_id : dbtComment.id, body : body } ); } else { await github.rest.issues.createComment( { owner : owner, repo : repo, issue_number : issue_number, body : body } ); } } PR上のコメントの一覧を取得して、既に DBT Compile Result: で始まるコメントがPR上に存在するのであれば、そのコメントのアップデート、存在しないのであれば新しくコメントをする。 という処理をしています。 条件分岐が発生する処理を簡便に記載したかったのでinlineでjavascriptを記載できる github-script を使用して記述しています これによってactionが走るたびにコメントが新規でされるのではなく、一つのコメントが上書きされ続けるような処理となり、PullRequestの可視性を損なわないようにしました。 どんなふうに動くかみてみる こんなモデルをテストで作ってみました {{ config( schema = ' sample_schema ' )}} SELECT ' hogehoge ' AS name , 30 AS amount UNION ALL SELECT ' fugafuga ' AS name , 50 AS amount {{ config( schema = ' sample_schema ' )}} SELECT SUM (amount) AS sum_amount FROM {{ ref( ' sample_model1 ' ) }} AS sample_model1 以下のようにPullRequest上に変更内容が折り畳まれた状態でコメントされて、ref関数のコンパイル結果がCI環境のデータセットになっていることを確認できます 次にsample_model1.sqlを以下のように修正して再度pushしてみます {{ config( schema = ' sample_schema ' )}} SELECT ' hogehoge ' AS name , 30 AS amount UNION ALL SELECT ' fugafuga ' AS name , 50 AS amount UNION ALL SELECT ' blabla ' AS name , 100 AS amount 以下のように既存のcommentがeditされて、修正後の内容でコンパイルした結果で上書きされていることが確認できます 結果 弊社のデータ基盤は four keys計測による開発ヘルススコアの計測 を行っており、今回の仕組みのリリース前後で開発リードタイムを計測してみましたが、目立った影響は出ていませんでした😢 レビューを円滑にできる環境を整っていないことが課題ではなく、他業務との兼ね合いでDWH開発のレビューに充てることができる時間がそもそも少なそうだったり、レビューに必要なドメインのインプットが足りていないことがボトルネックになっていそうだなという発見にも繋がったので、そこはプラスに捉えています メンバーの声を聞いていると便利なことは間違いないらしいので、レビューコストの低減による持続的なトイル削減に将来的には繋がっていけばいいなと思ってます🙏 使えそうだな。試してみようかなと思っていただけたら幸いです! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp
アバター
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の菊地です。 今回は、タイミーがBIツールとして導入しているLookerでの、 H3 を使用した可視化をするための取り組みを紹介したいと思います! H3とは H3 とは、Uber社が開発しているグリッドシステムで、オープンソースとして提供されています。 H3 では、位置情報に紐づいたイベントを階層的な六角形の領域にバケット化することができ、バケット化された単位でデータの集計が可能になります。 タイミーでは、サービスを提供する各都市の需給を測定するために,六角形単位で集計したデータを可視化するなど、様々な場面での分析に活用しており、例えば以下のような可視化を行なっております(数値はランダム値です)。 H3 についての詳細は、以下のページが参考になるかと思います。 https://h3geo.org/ https://github.com/uber/h3 https://www.uber.com/en-JP/blog/h3/ 前提条件 LookerのデータソースはGoogle Cloud BigQueryとします。 可視化に必要な各種ファイルの生成にはPythonを用いており、使用しているPythonバージョンと、依存ライブラリのバージョンについては、以下で動作確認を行っています。 Pythonバージョン: 3.11.4 依存ライブラリ geojson==3.0.1 h3==3.7.6 numpy==1.25.2 pandas==2.0.3 shapely==2.0.1 topojson==1.5 また、大変簡略化した例ですが、データソースのBigQueryプロジェクト・データセット・テーブルは、以下の想定とします。 プロジェクト名: sample-project データセット名: sample-dataset テーブル sales : 売上データを保持しているテーブル sales_at : 売上の日時 amount : 売上 place_id : places テーブルの外部キー places : 位置情報(緯度・経度)を保持しているテーブル places テーブルと sales テーブルには1:nのリレーションが存在 erDiagram places ||--|{ sales: "1:n" places { INTEGER id FLOAT latitude FLOAT longitude } sales { INTEGER id DATETIME sales_at INTEGER amount     INTEGER place_id } Lookerでの可視化を行うための手順 今回は以下の手順に従って、上記saleテーブルの売上をH3六角形にバケット化し、Looker上で可視化します。 緯度経度情報を保持しているBigQueryテーブルにH3六角形IDを付与し、別テーブルとして保存 TopoJsonファイルの作成 作成したTopoJsonファイルをLookerに追加 Lookerのmodelファイルにmap_viewフィールドを追加 Lookerのviewファイルにdimensionを追加 Lookerのmodelファイルにexploreを追加 Lookerでの可視化 1. 緯度経度情報を保持しているBigQueryテーブルにH3六角形IDを付与し、別テーブルとして保存 集計の際に使用する「緯度経度情報を保持しているBigQueryテーブル」(ここでは places テーブル)に対して、H3六角形ID(以下H3 hex idと記載)を付与し、別テーブルとして保存しておきます。ここでは h3_places テーブルとして保存しています。 下記は、 places テーブルをpandas.DataFrameとして読み込み、H3六角形解像度0~15までのH3 hex idを付与し、テーブルとして書き出すコードの例です。 H3六角形解像度は値が大きくなるにつれて、小さな六角形(=解像度が上げる)になり、詳細については下記ドキュメントが参考になるかと思います。 https://h3geo.org/docs/core-library/restable/ import h3 import pandas as pd class BigQueryClient : def __init__ (self): ... def read_table_as_dataframe (self, table_id: str ) -> pd.DataFrame: """BigQueryテーブルをpandas.DataFrameとして読み込む処理""" ... def write_table_from_dataframe (self, df: pd.DataFrame, table_id: str ) -> None : """pandas.DataFrameをBigQueryテーブルを書き込む処理""" ... def make_h3_hex_ids (df: pd.DataFrame) -> pd.DataFrame: _df = df.copy() for resolution in range ( 16 ): # 緯度・経度情報を元に、H3 hex idを付与 _df[f 'h3_hex_id_res_{resolution}' ] = df.apply( lambda x: h3.geo_to_h3(x[ 'latitude' ], x[ 'longitude' ], resolution), axis= 1 ) return _df if __name__ == '__main__' : ... bq_client = BigQueryClient() # 緯度(latitude)、経度(longitude)を保持しているBigQueryテーブルをDataFrameとして読み込む df = bq_client.read_table_as_dataframe( 'sample-project.sample-dataset.places' ) # H3 hex idを付与する h3_df = make_h3_hex_ids(df) h3_df.rename(columns= dict ( id = 'place_id' ), inplace= True ) # H3 hex idを付与したDataframeをBigQueryテーブルとして書き込み bq_client.write_table_from_dataframe(df=h3_df, 'sample-project.sample-dataset.h3_places' ) 例として、以下のような緯度経度を保持しているサンプルデータに、H3 hex idを付与した場合、以下のような結果になります。 import numpy as np np.random.seed( 42 ) tokyo_latitude = 35.6762 tokyo_longitude = 139.6503 df = pd.DataFrame( [[i, np.random.normal(tokyo_latitude, 0.3 ), np.random.normal(tokyo_longitude, 0.3 )] for i in range ( 1 , 11 )], columns=[ 'place_id' , 'latitude' , 'longitude' ] ) h3_df = make_h3_hex_ids(df) h3_df.head( 10 ) 2. TopoJsonファイルの作成 「1. 緯度経度情報を保持しているBigQueryテーブルにH3 hex idを付与する」でH3 hex idを付与したDataFrameを元に、TopoJsonファイルを作成します。 TopoJsonの詳細についてはこちらの「 topojson 」GitHubリポジトリを参照してください。 下記は、TopoJsonファイルを作成するコード例です。 処理の内容としては、GeoJson形式を経由して、TopoJsonに変換し、ファイルとして出力をしています。 TopoJsonファイルは、 H3解像度 別に作成しています。 from pathlib import Path import geojson import h3 import pandas as pd from shapely import geometry import topojson class H3ToGeojson : @ staticmethod def get_h3_geojson_features (h3_hex_ids: list [ str ]) -> list [geojson.Feature]: polygons = h3.h3_set_to_multi_polygon(h3_hex_ids, geo_json= True ) features = [geojson.Feature(geometry=geometry.Polygon(polygon[ 0 ]), properties= dict (h3_hex_id=h3_hex_id)) for polygon, h3_hex_id in zip (polygons, h3_hex_ids)] return features def get_h3_geojson_feature_collection_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str ) -> geojson.FeatureCollection: assert df.columns.isin([h3_hex_id_column]).any(), f 'column `{h3_hex_id_column}` is not exists.' unique_h3_hex_ids = df[h3_hex_id_column].unique().tolist() geojson_features = self.get_h3_geojson_features(unique_h3_hex_ids) feature_collection = geojson.FeatureCollection(geojson_features) return feature_collection class H3ToTopojson : def __init__ (self): self.h3_to_geojson = H3ToGeojson() def get_h3_topojson_topology_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str ) -> topojson.Topology: feature_collection = self.h3_to_geojson.get_h3_geojson_feature_collection_from_dataframe( df, h3_hex_id_column=h3_hex_id_column ) return topojson.Topology(feature_collection, prequantize= False ) def make_h3_topojson_file_from_dataframe (self, df: pd.DataFrame, h3_hex_id_column: str , save_file_path: Path) -> None : topojson_topology = self.get_h3_topojson_topology_from_dataframe(df=df, h3_hex_id_column=h3_hex_id_column) topojson_topology.to_json(save_file_path) if __name__ == '__main__' : ... h3_to_topojson = H3ToTopojson() save_dir = Path( 'topojson' ) save_dir.mkdir(exist_ok= True ) for resolution in range ( 0 , 16 ): h3_hex_id_column = f 'h3_hex_id_res_{resolution}' h3_to_topojson.make_h3_topojson_file_from_dataframe(df=h3_df, h3_hex_id_column=h3_hex_id_column, save_file_path=save_dir / f '{h3_hex_id_column}.json' ) 例として、先ほど作成したサンプルデータに対して、 resolution=4 を指定してTopoJsonファイルとして書き出す処理は以下のようになります。 h3_to_topojson = H3ToTopojson() h3_to_topojson.make_h3_topojson_file_from_dataframe(h3_df, resolution= 4 ) TopoJsonファイルの中身は以下のようになります。 { " type ":" Topology "," objects ": { " data ": { " geometries ": [{ " properties ": { " h3_hex_id ":" 842f5a3ffffffff " } ," type ":" Polygon "," arcs ": [[ -5 , -2 , 0 ]] ," id ":" feature_0 " } , { " properties ": { " h3_hex_id ":" 842f5bdffffffff " } ," type ":" Polygon "," arcs ": [[ 1 , -4 , 2 ]] ," id ":" feature_1 " } , { " properties ": { " h3_hex_id ":" 842f5abffffffff " } ," type ":" Polygon "," arcs ": [[ 3 , 4 , 5 ]] ," id ":" feature_2 " }] ," type ":" GeometryCollection " }} ," bbox ": [ 139.198358 , 35.267135 , 140.126313 , 36.103519 ] ," arcs ": [[[ 139.44526 , 35.765969 ] , [ 139.458427 , 36.000295 ] , [ 139.695196 , 36.103519 ] , [ 139.918545 , 35.971536 ] , [ 139.903854 , 35.7366 ]] , [[ 139.44526 , 35.765969 ] , [ 139.667342 , 35.634256 ]] , [[ 139.653549 , 35.399825 ] , [ 139.419179 , 35.297723 ] , [ 139.198358 , 35.429167 ] , [ 139.21065 , 35.662982 ] , [ 139.44526 , 35.765969 ]] , [[ 139.653549 , 35.399825 ] , [ 139.667342 , 35.634256 ]] , [[ 139.667342 , 35.634256 ] , [ 139.903854 , 35.7366 ]] , [[ 139.903854 , 35.7366 ] , [ 140.126313 , 35.603627 ] , [ 140.111006 , 35.368594 ] , [ 139.874758 , 35.267135 ] , [ 139.653549 , 35.399825 ]]]} 3. 作成したTopoJsonファイルをLookerに追加 LookerのFileBrowserを開いて、先ほど作成したTopoJsonファイルを追加します。 追加後、適切なフォルダにファイルを移動します。ここでは maps/h3 フォルダにTopoJsonファイルを移動します。 ├── maps    └── h3    ├── h3_hex_id_res_0.topojson    ├── h3_hex_id_res_1.topojson    ├── h3_hex_id_res_2.topojson ...    └── h3_hex_id_res_15.topojson 4. Lookerのmodelファイルにmap_viewフィールドを追加 下記のようにmap_layerを設定します。map_layerは H3解像度 別に設定しています。 property_key は「2. TopoJsonファイルの作成」で使用している H3ToGeojson.get_h3_geojson_features メソッド内の geojson.Feature の引数で設定している properties のkey名である h3_hex_id を指定しています。 map_layer: h3_hex_id_res_0 { file : "/ maps / h3 /h3_hex_id_res_0. topojson " format : topojson property_key: " h3_hex_id " } map_layer: h3_hex_id_res_1 { file : "/ maps / h3 /h3_hex_id_res_1. topojson " format : topojson property_key: " h3_hex_id " } map_layer: h3_hex_id_res_2 { file : "/ maps / h3 /h3_hex_id_res_2. topojson " format : topojson property_key: " h3_hex_id " } ... map_layer: h3_hex_id_res_15 { file : "/ maps / h3 /h3_hex_id_res_15. topojson " format : topojson property_key: " h3_hex_id " } 5 . Lookerのviewファイルにdimensionを追加 map_layer_nameは、「4. Lookerのmodelファイルにmap_viewフィールドを追記」で作成した、map_layer名を指定します。 dimensionは H3解像度 別に設定しています。 view : h3_places { sql_table_name: ` sample - project . sample - dataset .h3_places` ;; dimension : h3_hex_id_res_0 { group_label: " H3 " group_item_label: " H3 解像度0の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_0 ;; map_layer_name: h3_hex_id_res_0 } dimension : h3_hex_id_res_1 { group_label: " H3 " group_item_label: " H3 解像度1の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_1 ;; map_layer_name: h3_hex_id_res_1 } dimension : h3_hex_id_res_2 { group_label: " H3 " group_item_label: " H3 解像度2の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_2 ;; map_layer_name: h3_hex_id_res_2 } ... dimension : h3_hex_id_res_15 { group_label: " H3 " group_item_label: " H3 解像度15の六角形 ID " type : string sql : $ { TABLE } .h3_hex_id_res_15 ;; map_layer_name: h3_hex_id_res_15 } } 6 Lookerのmodelファイルにexploreを追加 下記のようにexploreを追加します。 explore : sales { label : " sales " ... join : h3_places { view_label: " place " type : inner sql_on: $ { sales .place_id } = $ { h3_places.place_id } ;; relationship : many_to_one } } 7. Lookerでの可視化 作成したexploreでマップでの可視化を行うと、地図上にH3六角形メッシュが表示され、メッシュ毎にバケット化された集計値を色で表現することができます。 下記は東京近郊のデータを H3解像度 7のdimensionを使用して可視化した例です(数値はランダム値です)。 今回作成した H3解像度 dimensionを変更することで、目的に合わせて六角形メッシュの大きさを変更して可視化を行うことが可能です。 まとめ 今回は、Uber社がオープンソースとして提供している H3 を使用して、Looker上で可視化を行う方法について解説しました。 タイミーでは今回紹介したLookerでの可視化以外にも、機械学習の特徴量作成時に使用するなど、様々な場面で H3 を活用しています。 今後も地理情報を活かした分析をする際に活用していきたいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! 現在募集中のポジションは こちら です! 「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう! mermaid.initialize({startOnLoad: true});
アバター
はじめに マッチング領域、ワーキングリレーションチームの @Juju_62q です。 タイミーでは有志のメンバーが集まって1年半ほど前から輪読会を行っています。 現在5冊の書籍を読み終わっています。 ブログのタイトルにもありますが、今回 エンタープライズ アプリケーション アーキテクチャパターン (略称PoEAA)の輪読を実施しました。 書籍選定には以下のような狙いを置いていました。 複雑な ドメイン をソフトウェアで扱うための術を身につける アクティブレコードパターンにより詳しくなり、 Rails を上手く扱えるようになる 書籍に出てくる デザインパターン をきっかけとしてタイミーの アーキテクチャ について議論し、実験のきっかけを作る。 輪読会の方法 週1でmiroを利用して実施しています。 輪読会は参加者の「わからなかったこと」、「話してみたいこと」を掘り下げるような形で実施しています。 また、特別取り上げることは少ないですが、個人の感想や勉強になった場所を書けるようになっています。 「わからなかったこと」では参加者が書籍を読んでわからなかったことについて話します。 これは少し理解が曖昧な部分について、みんなで解説を行い理解を深めている様子です。 「話してみたいこと」ではタイミーの話や、ソフトウェアエンジニアリング一般で用途や使い分けについて話しています。 下記は「重ロック」の概念をタイミーのマッチングロジックに適用できないか話している様子です。 感想はそれぞれが自由に書いています。 輪読会では「参加者が少しでも参加前より理解できること」を目的に以下のような心構えを置いています。 本輪読会は読んだ人の疑問や議論したいことに基づいて意見交換をする形をとっています。したがって、参加者の協力がなければ良いものになることはありません。チームの学びを最大化するためにもあなたの思ったことを教えてください。 発言の正しさや凄さに関わらず、疑問、自分の意見を発言することに対して常にポジティブです。 ファシリテーター 及び参加者の皆さんはどんな意見も歓迎し、発言したことにまず感謝をするように努めてください。 特に今回は解釈が難しい書籍だったと思うので、「わからない」が主張しやすいことは大事だったと思っています。今後も参加者の「わからない」を大事にして運用していきたいと考えています。 学びや感想 岡野 Web Application Frameworkがよくできているというのを強く感じることができる書籍でした。 書籍の中に出てくる デザインパターン や考え方はタイミーでも実際に活かせることが多く、具体のテーブルやモデルについて議論ができるのはとても良い体験だったと思っています。 ActiveRecord と ドメイン モデルの関係とかの話が2002年の段階で整理されていたのは本当にすごいです。 難波 タイミーのバックエンドは Ruby on Rails を使っているので、この本を読むことでActive Recordの複雑さや大変さについて認識を新たにすることができたのはとても良かったです。書籍が発行された時代を感じるような課題意識や制約も多々ありますが抽象的に捉えることで現代でも参考になる部分はたくさんあったので、こういった過去の名著を一通り読んでみるのはオススメです。それにしてもLockは考えることが多くて本当に大変。 須貝 いつかは読みたいと思いながらなかなか読めずにいたPoEAAにチャレンジできて本当に良かったです。機会を作ってくれた輪読会主催メンバーに感謝しています。 書籍の中で紹介されているパターンが開発者間での共通言語となり「これは重オフラ インロック では?」といった会話がSlack上で発生することもありました。自分としてはまだまだ理解できていない部分もたくさんあるので今後も読み返していきたいと思います。 texta.fmの最初のほうのエピソードを聴くと Ruby on Rails との関係性も含めてさらに理解が深まるので本当におすすめです。自分はかなり助けられました。 終わりに PoEAAは2002年とそれなりに古い書籍ではありますが、考え方は現代にも通じる良い本です。 ここに書かれている設計パターンは Rails でもたくさん実装されています。 複雑なビジネスを扱う上で強力な武器になると思うので、みなさんぜひ一度読んでみてください。
アバター
こんにちは、Androidエンジニアのはる( @ haru2036 )とシャム( @arus4869 )です! 私たちは2023年6月8-9日にアメリカ合衆国サンフランシスコで開催された droidcon San Francisco 2023 に参加してきました。 前回のイベント報告編に引き続き、実際のセッションを紹介していきたいと思います。 tech.timee.co.jp 特に気になったセッション(haru編) Mobile Feature Flags and Experiments at Uber はじめに取り上げるのはUberのMahesh HadaさんとArun Babu A S PさんによるUberでのFeature Flag運用に関するセッションです。 Uberでは大量のFeature FlagやExperimentalなFeatureを管理するために独自のFeature Flag自動生成などの取り組みを行っているそうで、そういったFlagのFetchをするタイミングもサービスの性質上「大きく位置情報が変化したとき」など特殊なものがあるということでした(日本から渡航してきたばかりの私の端末上でもそれをトリガーとしたFlagのFetchが走ってたんだなあ、と謎の実感を持ちながら聞いていました)。 また、そういったFlagによって問題が起きた時にできる限り早くロールバックするためにFCMを用いて緊急ロールバック用メッセージを送信しているという話を聞いた時はなかなか衝撃を受けました。 タイミーでもFirebase Remote Configなどを使ったFeature Flag管理を行っているのですが、それとは桁外れに大規模で即時性の高い管理が行われていてさすがだなと思いました。 上記の緊急ロールバックの話など、世界で展開している大きなサービスならではの手法を知ることができたセッションでした。 Unlocking the Power of Shaders in Android with AGSL and Jetpack Compose 次に取り上げるのはRikin MarfatiaさんのAGSLをJetpack Composeで使うセッションです。 AGSLはAndroid上で利用できるシェーダ言語で、GPUを使用して画面を描画することができます。 私は趣味でUnityなどでシェーダを扱っていたのですが、それとよく似た記法のAGSLを用いることで視覚的にリッチなUIを実現することができるそうでした。 実際にデモとして紹介されていたUIはボタンが電球のように光ったり、写真ギャラリーの切り替え時に色収差を発生させるなどのとても派手な表現でしたが、それをCompose上で簡単に実現できることに感動しました。 AGSLについてはこちら↓ developer.android.com Reimagining text fields in Compose 最後に取り上げるのはGoogleのZach KlippensteinさんによるJetpack ComposeにおけるTextFieldの成り立ちとこれからについてのセッションです。 多くのプロダクトと同様に、MVPから始めてユーザースタディを繰り返しながら開発していったそうなのですが、これまた多くのプロダクトと同様に現在では技術的負債も貯まってきてしまっているそうです。 そこで、それを解消すべく大きくAPIを変更したBasicTextField v2を開発しているそうです。ユーザースタディ参加者も募集中だよ!とのことです。参加してみたいなあ。 GoogleでのCompose開発の舞台裏と、TextFieldの未来を同時に知ることができる一粒で二度美味しいセッションでした。 speakerdeck.com 特に気になったセッション(syam編) 次にsyamが特に気になったセッションを紹介していきます! Navigating the Unknown: Tips for Efficiently Learning a New Codebase ADAM GREENBERGさんによるセッションを紹介します。 新しいコードベースを理解し、その知識を活用するための具体的な手法と戦略について深く掘り下げたセッションでした。 ADAM GREENBERGさん は、自身の経験を基に、コードベースの理解、ドキュメンテーションの作成、そしてその知識の共有という3つの主要なステップを中心にお話ししていました。 以下にセッションで触れた3つの重要なステップについて触れます。 1. コードベースの理解 新しいプロジェクトや既存のコードベースに取り組む開発者にとって、コードベースの理解は非常に重要なステップである。コードベースの理解を促進させるためにはアーキテクチャ図の作成やデバッグツールの使用などが重要であると話していました。 2. ドキュメンテーションの作成 ドキュメンテーションをすることは、学んだこと振り返ったりを他の人々と共有するための重要なステップであると説いています。アーキテクチャ図の作成、重要な機能の説明、特定のコードスニペットの説明など必要に応じてドキュメンテーションする必要があると話していました。 3. 知識の共有 他の開発者が自分の知識を利用し、コードベースをより効果的に利用するための重要なステップです。このステップがチーム全体の生産性と効率を向上させ、個々の開発者が自身の理解を深めるのに役立つと述べました。 このセッションは、新しいコードベースを効果的に理解し、その知識を活用・共有するために大事なポイントをお話ししていました。 ちょうど僕たちのチームもオンボーディングや共通認知をとるためのREADMEを作成していることもあり、当たり前のことかもしれませんが、改めて聞くことができたのでこのセッションは有用でした。 Find your way with GoogleMap() {} 次は、 BRIAN GARDNERさんによるセッションを紹介します。 このセッションでは、その可能性を具体的に示すために、マップの表示、マーカーの追加、そしてクラスタリングといった基本的な機能がどのように簡単に実装できるのかを学びました。 特に印象的だったのは、大量のマーカーを効率的に表示するためのクラスタリング機能です。マーカーの数が増えると、地図が混雑し、ユーザーが特定のマーカーを見つけるのが難しくなります。しかし、クラスタリングを使用することで、近接するマーカーを一つのクラスタとして表示でき、地図の見やすさとパフォーマンスを向上させることができるとのことです。 また、このセッションでは、具体的な実装方法を確認するためのソースコードも提供されました。 JetpackComposeのGoogleMapを使用する際にいくつか注意点も述べていました。 クラスタリングについての注意点として、MarkerをClustering内で使用しないようにとの警告がありこれは、 IllegalStateException を引き起こす可能性があるそうです。 また、パフォーマンスを向上のために MapsInitializer.initialize(context, MapsInitializer.Renderer.LATEST) を使用して、最新のレンダラーを利用することを推奨していました。 さらに、Paparazziテストで、特にプレビューモードでの早期リターンを利用して、レイアウトを保持するためのBoxを返すと良いとのことでした。 ANR問題についても触れられていましたが、具体的な解決策は聞き取れなかったので、調べてみようかなと思います。 ちょうどGoogleMap周りをコンポーズ化している時だったので、とても有り難かったです。 セッションスライド speakerdeck.com ソースコード github.com Animating content changes with Jetpack Compose 最後に KINNERA PRIYA PUTTIさんによるセッションを紹介します! Jetpack Compose for Desktopを使ってスライドを作っており、随所にアニメーションが動くすごいプレゼンテーションでした! KINNERA PRIYA PUTTIさん は自身の実践を踏まえ、UIアニメーションをより魅力的にするための各種アニメーションテクニックについて語ってくれました。 以下にセッションで紹介された注目すべきテクニックについて触れていきます。 1. CrossFadeによるコンテンツ間の切り替え 新旧の画面や要素間のトランジションを滑らかにするCrossFadeは、ユーザーがアプリの使用中に感じる違和感を軽減するための非常に重要な要素です。彼は、これがいかに自然なユーザー体験を生むかについて語っていました。 2. animateContentSizeによるコンテンツサイズのアニメーション化 コンテンツのサイズを変更する必要がある場合、animateContentSizeを用いるとその変更がなめらかになります。これは、ユーザーが自由にコンテンツのサイズを調整できるようにするための重要なステップであると彼は指摘していました。 3. 各種UI要素に対するアニメーションの追加 リスト、詳細画面、ナビゲーションドロワー、ボトムシート、ダイアログなどのUI要素にアニメーションを追加することで、ユーザーフレンドリーなUIを実現する方法について具体的に語られました。 このプレゼンテーションは、Jetpack Composeを活用して、ユーザーにとって使いやすく、魅力的なUIアニメーションを創出するための重要なポイントを提供していました。 我々のチームもUI開発におけるアニメーションの活用に取り組んでおり、このような新たな視点やアイデアを得られることは非常に有意義でした。 まとめ 今回はセッションの内容をいくつか抜粋して紹介させて頂きました。 もしより詳しい内容や海外カンファレンスの様子に興味を持っていただけたら、カジュアル面談でお話しすることもできますので是非一度お話ししましょう! product-recruit.timee.co.jp
アバター
はじめに こんにちは、Androidエンジニアのはる( @ haru2036 )とシャム( @arus4869 )です! 私たちは2023年6月8~9日にアメリカ合衆国サンフランシスコで開催された droidcon San Francisco 2023 に参加してきました。 タイミーでは KaigiPass というカンファレンス参加補助制度があり、その海外参加第一号として私たちが参加してきた形になります。 私たちが開発・運営している タイミー は、ワーカーさんが利用するためにモバイルアプリが必要不可欠なサービスとなっており、その開発に関する知見を広く得るために参加してきました。 テック企業が集まるサンフランシスコでのカンファレンスは内容もそうですが情報の新鮮度といういう意味でも良い刺激になり、とても有意義な経験をすることができました。 会場のようす 会場となったカリフォルニア大学サンフランシスコ校は、医学分野が主になっているそうで、カンファレンスセンターのすぐ隣にリハビリセンターのようなものがあったりしました。そう言った意味ではソフトウェアのカンファレンス会場としては結構異色なのではないでしょうか。 また、国内のカンファレンスと比べてスポンサーの層が結構違うという感想を抱きました。 国内カンファレンスでは多くの場合Androidプラットフォーム上でサービスを展開している事業会社がスポンサーをしているケースが多いのですが、それと比較するとSentry, Bitrise, DataDogなどほとんどの会社が事業会社に対して開発をサポートするサービスを提供している会社だったのが興味深かったです。 https://twitter.com/arus4869/status/1666885385597898752?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1666885385597898752%7Ctwgr%5Eb44440b405611dde3ca583c63950f1096c7d8b21%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fwww.notion.so%2Ftimee%2Fd5096f7c9c0647b2a036b7393917e7b7 My first time abroad was #dcsf23 and it was really good. Thanks to the very helpful sessions and kind people! pic.twitter.com/MhtTpYF2ms — はる (@haru2036) 2023年6月10日 実際にブース出展している様々な会社のノベルティをいただきました。 droidcon sanfransiscoTシャツは入場者特典でみんなもらえるみたいです! すごく靴下が多い印象でした。 どこの会社のステッカーも良かったのですが、droidconステッカーが中でも嬉しかったです また、会場では一日を通して食事や飲み物が提供されていたのですが、全て屋外で提供されており参加者の憩いの場となっていました。私(はる)も朝は顔くらいのサイズがあるクロワッサンをかじりながらコーヒーを飲んでいました(笑) ランチの時間の屋外では見知らぬ人でも活発に話しかけて盛り上がっているところも多く、私も初めて会う方とお話しすることが多かったです。 実際のセッションの内容については後日投稿するセッション内容編をご覧ください! tech.timee.co.jp 初めての海外カンファレンスに参加してみて 今回初めて海外カンファレンスに参加してみた私たちでしたが、印象的だったのは先述のスポンサーの違いだけではありません。セッション自体の内容も、全世界でサービスを提供しているUberのような企業による実際の実装の解説や、Androidというプラットフォーム自体を提供しているGoogle自身によるフレームワーク自体についての解説、今後の予定の発表など、一次情報に直接触れている感覚がとても強いイベントでした。 また、日本でも「この中で⚪︎⚪︎使ってる方どのくらいいらっしゃいますかね?」と質問する発表者の方は多いですが、今回のイベントではさらに多くのスピーカーがそう言った質問を投げかけたりしていました。 また、結構な確率でジョークを挟んでくるスピーカーが多く、この辺りの話術は自分が発表する際にも見習いたいな、と感じました。 また、今回のトレンドとして多くセッションがあったのはJetpack Composeに関する話題で、やはり世界中のAndroid開発者が興味を持っているトピックなんだなと感じました。 おまけ せっかくサンフランシスコに行ったので、近くにあるいくつかのとても行きたかった場所に行ってきました! Computer History Museum (マウンテンビュー) 興奮のあまり建物の写真を撮り忘れました…… コンピュータに関わる人なら人生で一度はぜひ行ってもらいたいComputer History Museum。念願かなっていくことができました。そろばんや計算尺などの道具から始まり、コンピュータ科学の父と呼ばれているアラン・チューリングが解読したエニグマ暗号機の実機や、GUIを初めて搭載したコンピュータであるXerox Alto、iPhoneのご先祖様と言えなくもないNewton MessagePad, そして2021年に発売されたばかりのAI用超巨大プロセッサ、Cerebras Wafer Scale Engine 2などコンピュータの始まりから現在に至るまでのエポックメイキングなコンピュータたちが多数収蔵されていました。 周りを少し散策するだけでGoogleのロゴが入った建物だらけのエリアに迷い込んでしまうので、これぞシリコンバレー!という感じの場所でした。 Twitter X本社 当時は、この看板が外れるとは思いませんでしたが、Twitterの看板が外れる前に見ることができてよかったです! まとめ 今回の海外カンファレンスへの参加では、帰国してすぐにコードベースの改善に活かせる情報から、これからのAndroidを取り巻く情報までの幅広い知見たちだけではなく、実際にその場にいることによって得られる肌感覚のようなものや海外の開発者との新しい出会いなど、絶対にその場にいないと得られないものをたくさん持ち帰ってくることができたと思います。 また、タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非一度お話ししましょう! product-recruit.timee.co.jp
アバター
こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで ( @pokohide ) です。 前回はRails edgeでCIを回し始めた話を紹介しました。 tech.timee.co.jp 今回は、実際に弊社でCIをRails edgeで回し始めた事で見つけたエラーの例を紹介していきます。記事公開時点(2023年7月)のバージョンは下記の通りです。 $ ruby -v ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux] $ rails -v Rails 7.0.6 ActiveRecord::DangerousAttributeError object_id is defined by Active Record このエラーに関する参考記事はこちらです。 euglena1215.hatenablog.jp 一部のモデルで object_id というカラム名を定義していたため以下のようなエラーが発生し、そのレコードを生成しているテストが軒並み落ちました。 ActiveRecord::DangerousAttributeError: object_id is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name. # /usr/local/bundle/gems/factory_bot-6.2.1/lib/factory_bot/decorator/new_constructor.rb:9:in `new' # /usr/local/bundle/gems/factory_bot-6.2.1/lib/factory_bot/decorator.rb:16:in `send' ... Objectクラスで、オーバーライドされると予期せぬ影響を与えうるメソッド名と同じ名前を利用できなくなったからでした。 object_id に限らず dup , freeze , hash , class , clone なども利用できなくなるのでそういったカラム名を使わないようにする必要があります。 このエラーは object_id を object_identifier とリネームする事で対応しました。 Rails7.0.5.1からの create_association の挙動変化によるエラー 巷で話題のcreate_associationの挙動変更によるエラーです。既存レコードが存在する場合にユニーク制約によりバリデーションエラーが発生するといってテストケースがありましたが、そちらのテストが落ちました。Rails edgeを回し始めた頃はRails v7.0.4だったため、このエラーに遭遇しました。 Rails v7.0.5.1から create_association の挙動が「別々のトランザクションでinsertしてからdeleteする」から「同一トランザクションでdeleteしてからinsertする」に変わりました。この影響により、DBのレコードに依存するバリデーション(例: validates_uniquness_of )が効かなくなりました。この挙動変化に関する内容はこちらをご参考ください。 blog.willnet.in parser gemを利用してcreate_associationの呼び出し箇所を調査し、影響範囲を一つずつ確認、必要な箇所に条件文を追加するなどして対応しました。 既にリリースされている変更ではありますが、Rails edgeでCIを回し始めたことで早期に問題を発見でき、create_association削除する前に検証を挟む新しいオプションの提案をrails/railsへのPR *1 を通して行うなどしました。 NoMethodError: undefined method `reading_role' for ActiveRecord::Base:Class Rails7.1から ActiveRecord::Base.reading_role がなくなるため、この記述を行っていた箇所のテストが落ちました。 irb(main): 002 : 0 > ActiveRecord :: Base .reading_role { " severity " : " WARN " , " message " : " DEPRECATION WARNING: ActiveRecord::Base.reading_role is deprecated and will be removed in Rails 7.1. \n Use `ActiveRecord.reading_role` instead. \n (called from xxxxx) " } => :reading irb(main): 003 : 0 > ActiveRecord :: Base .writing_role { " severity " : " WARN " , " message " : " DEPRECATION WARNING: ActiveRecord::Base.writing_role is deprecated and will be removed in Rails 7.1. \n Use `ActiveRecord.writing_role` instead. \n (called from xxxxx) " } => :writing この問題に関しては元々出ていたWarningの内容に従い、 ActiveRecord.reading_role を代わりに使う事で対応しました。 partialsにインスタンス変数をlocalsとして渡す挙動が削除されたことによるエラー 弊社では請求書(PDF)を生成するために ActionController::Base#render_to_string を用いてHTML文字列を取得しています。その処理の中でpartialsにインスタンス変数をlocalsとして渡していましたが、そこでエラーが発生しました。 今までWarningが出ていた内容ではありますが、Rails7.1からインスタンス変数をlocalsで渡せなくなったためです *2 。 インスタンス変数ではなくローカル変数として渡すことで対応を予定しています。 before_type_castの返り値の型が変わったことによるエラー # create_table :posts, force: true do |t| # t.integer :foo, default: 1, null: false # end class Post < ActiveRecord :: Base enum foo : { x : 1 , y : 2 } end Integer型でEnumを定義しているカラム foo を持つモデルのレコードに対して record.foo_before_type_cast で参照すると、元々の環境ではInteger型で返っていたものが、Rails edge環境下ではString型で返るようになったため落ちているテストケースを見つけました。 record = Post .new( foo : :x ) # 検証当時の環境(Rails v7.0.6) record.foo_before_type_cast => 1 # Rails edge record.foo_before_type_cast => " 1 " 今回のエラーを再現するコード *3 を用意し、 git bisect *4 を利用して二分探索で挙動が変わったをコミットを調査しました。その後のコミットやPRを追ったところ、rails v7.1からDBの型で値を取得する *_for_database が追加されていることに気づいたため、その実装をバックポートして使うように修正することで対応を行いました。 # frozen_string_literal: true case Rails :: VERSION :: STRING when /^ 7 \. 0 / # 以下の定義を読み込むために何もしない。 when /^ 7 \. 1 / # v7.1 には含まれているため読み込まない。 return else # v7.2 以降で削除し忘れないように例外を投げる。 raise ( ' Consider removing this patch ' ) end module ActiveRecord module AttributeMethods module BeforeTypeCast # refs: https://github.com/rails/rails/pull/46283 def read_attribute_for_database (attr_name) name = attr_name.to_s name = self .class.attribute_aliases[name] || name attribute_for_database(name) end end end end 最後に 今回は、Rails edgeでCIを回すことによって見つけた将来動かなくなるコードの早期発見やその対応、原因についての簡単な解説を行いました。また紹介しきれていないですが、エラーだけでなくRails 7.2からの廃止予定を知らせるWarningもいくつか確認できました。 Rails edge導入当初は112個のテストケースが落ちましたが、徐々に対応を行なっていき落ちるテストケースは22件にまで減りました。こういった活動を継続することでRubyやRailsのコミュニティの進化に追随できるので引き続き頑張っていこうと思います。 タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非お話ししましょう! product-recruit.timee.co.jp *1 : https://github.com/rails/rails/pull/48643 *2 : https://github.com/rails/rails/commit/8241178723d02123734a1efd01c12b9fda2f4fea *3 : https://gist.github.com/pokohide/b310ea180e7de0467360c96debbb8363 *4 : https://git-scm.com/docs/git-bisect
アバター
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の小関です。 今回はDSグループがMLパイプライン構築時に活用しているVertex AI Pipelinesを効率的に開発するための取り組みを紹介したいと思います! Vertex AI Pipelinesとは Vertex AI Pipelines とは、Google Cloudが提供しているMLパイプラインをサーバーレスに構築・実行できるサービスです。 Vertex AI Pipelinesを活用することで、下記のようなデータをBigQeuryから取得し、特徴量の作成・データセットの分割後、モデルを学習するようなML パイプラインが比較的容易に構築できます。 Vertex AI Pipelinesで構築したMLパイプラインのサンプル Vertex AI Pipelinesの活用事例と挙げられた改善点 タイミーのDSグループでは、下記のようなML パイプラインをVetex AI Pipelinesで開発・運用しています。 ワーカーに対して、おすすめの募集を出力するパイプライン クライアントの離脱を予測するパイプライン 各種KPIを予測するパイプライン ML パイプラインを構築していく上で、以下のような改善点が挙げられていました。 パイプラインのリポジトリのディレクトリ構成や、CI/CDを共通化したい Google Cloud上での処理を共通化し、より使いやすい形で処理を呼び出せるようにしたい 各パイプラインに必ず入れ込む必要があるKubeflow Pipelines *1 の記述を共通化したい Vertex AI Pipelinesを効率的に開発するための取り組み 挙げられた課題に対して、DSグループでは以下の3つの取り組みを行なっています。 1. Vertex AI Pipelines開発用のテンプレートリポジトリの構築 cookiecutter を使用して、パイプライン開発に関わるディレクトリや、CI/CDに用いるyaml, shell scriptを生成してくれるテンプレートを作成しました。パイプラインの開発開始時にこのテンプレートを利用しています。 下記のようにcookiecutterコマンドでプロジェクトを生成し、プロジェクト名・プロジェクトの説明・Pythonのバージョン・作成者を入力することで、開発用のテンプレートが生成されます。 $ cookiecutter [開発用のテンプレートリポジトリのパス] > project_name [project_name]: > project_description []: > python_version [3.10.1]: > author [timee-dev]: # Vertex AI Pipelines開発用のテンプレート . ├── .github │   ├── PULL_REQUEST_TEMPLATE.md │   └── workflows │   ├── CI/CDのyamlファイル ├── .gitignore ├── Makefile ├── README.md ├── pyproject.toml ├── src │   └── pipeline_template │   ├── components │   │   └── component │   │   ├── パイプラインを構成するコンポーネントのソースコードとDockerfile │   ├── pipelines │   │   ├── パイプラインをコンパイル、実行するためのソースコード │   ├── pyproject.toml └── tests ├── テストコード 2. Google Cloudの処理を集約した社内ライブラリの構築 Google CloudのPythonライブラリ をラップして、BigQueryでのクエリ実行・クエリ結果のDataFrame化・テーブルの書き込みや、Cloud StorageにおけるファイルのI/O処理などを行える社内ライブラリを構築しています。こちらの社内ライブラリは、DSグループ全体で保守・運用を行なっており、バージョン管理とデプロイの自動化をした上で、Artifact Registryにプライベートパッケージとして置いて利用しています *2 。この社内ライブラリに関しては、Vertex AI Pipelinesに限らずVertex AI WorkbenchやCloud Runなど、他のGoogle Cloudのサービスでの実装でも活用されています。 簡単な利用例として、SQLファイルのクエリを実行して、 pd.DataFrame として取得する処理を紹介します。 # 社内ライブラリからBigQuery関連の処理をまとめているクラスをimport from [社内ライブラリ名].bigquery import BigQueryClient # project_idにGoogle Cloudのプロジェクト名、sql_dirにSQL fileを格納しているディレクトリ名を指定 bq_client = BigQueryClient(project_id=PROJECT_ID, sql_dir=SQL_DIR) # test.sql内でJinjaテンプレートで定義されているパラメーターをquery_paramsで受け取り、クエリの実行結果をpd.DataFrameとして受け取る test_df = bq_client.read_gbq_by_file( file_name= 'test.sql' , query_params= dict (loading_start_date=LOADING_START_DATE, loading_end_date=LOADING_END_DATE), ) -- SQL_DIR/test.sql DECLARE LOADING_START_DATE DEFAULT ' {{ loading_start_date }} ' ; DECLARE LOADING_END_DATE DEFAULT ' {{ loading_end_date }} ' ; SELECT * FROM `project_id.dataset_id.table_id` WHERE event_data BETWEEN LOADING_START_DATE AND LOADING_END_DATE 3. Kubeflow Pipelinesにおいて共通化できる処理を集約した社内ライブラリの構築 コンポーネント間のアーティファクトの受け渡し・Cloud StargeへのI/O処理や、yamlで定義されたコンポーネントの情報を取得してくる処理 *3 などを行える社内ライブラリを構築しています。こちらもDSグループ全体で保守・運用を行なっており、バージョン管理とデプロイの自動化をした上で、Artifact Registryにプライベートパッケージとして置いて利用しています。 利用例として、学習データを受け取り、それを特徴量とターゲットに分割するコンポーネントにおけるアーティファクトの受け渡し・Cloud StargeへのI/O処理の実装を紹介します。 # pipeline_name/components/component_name/src/main.py from dataclasses import dataclass import pandas as pd from [社内ライブラリ名].artifacts import Artifacts from [社内ライブラリ名].io import df_to_pickle @ dataclass class ComponentArguments : train_dataset_path: str @ dataclass class OutputDestinations : x_train_path: str y_train_path: str def main (args: ComponentArguments) -> pd.DataFrame: train_dataset = pd.read_pickle(args.train_dataset_path) x_cols = [ 'x1' , 'x2' ] y_col = [ 'y' ] X_train, y_train = train_dataset[x_cols], train_dataset[y_col] return X_train, y_train if __name__ == '__main__' : # アーティファクトのパスを取得 artifacts = Artifacts.from_args(ComponentArguments, OutputDestinations) # インプットとなるアーティファクトのパスを受け取り、main関数を実行 X_train, y_train = main(artifacts.component_arguments) # パイプラインのアーティファクトを管理するCloud Storageのバケットへpickle形式でX_train, y_trainを書き込む df_to_pickle(artifacts.output_destinations.x_train_path, X_train) df_to_pickle(artifacts.output_destinations.y_train_path, y_train) 取り組みから感じたメリット 上で挙げた取り組みを通じて、グループ全体で感じている主なメリットを紹介していきます。 開発のスピードが上がる。特にパイプラインのテンプレートが用意されている事で、開発の初動が大幅に速くなりました。 テンプレートやライブラリを通して、ファイル構成や処理に共通知があるので、メンバー間でのレビューがしやすくなっている。 個別に開発した処理を社内ライブラリに追加していくことで、グループ全体の資産として蓄積している。 属人化されているコードが減っていくので、新規メンバーのキャッチアップがし易くなる。 今回紹介した取り組み以外にも、MLパイプラインのソースコードのモノリポ化などDSグループでは常に社内のMLOps基盤を強固にしていく活動を続けています! We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています!! product-recruit.timee.co.jp *1 : Kubeflow Pipelines SDK で実装したパイプラインをVertex AI Pipelinesで動作させています *2 : Artifact RegistryのプライベートパッケージをPoetryで扱う方法は こちら が参考になります *3 : Kubeflow Pipelinesにおけるパイプラインの定義ファイルで使用します
アバター
DREグループの 石井 です。 先日といってももうしばらく前ですが、Techmeeというイベントで生産性についてトイルの計測をしてそれを一定に抑える取り組みをしているという話をさせて頂きました 。 https://timeedev.connpass.com/event/275750/ www.youtube.com これはDREグループ内の生産性を維持する=一定以上のアウトプットが出せる状態を作り出すための取り組みでした。 しかし、生産性といえば、古くはコード量で計測されていたような(最も、コード量は良い指標ではありませんが)ものや今で言えばfourkeysといったようなもので、どれだけアウトプットに繋がっているかが大切になります。 私の所属するDREグループでは、主に色々なデータソースからデータを抽出し、便利に分析しやすい状態にすることに責任を持つと同時に、データ基盤の運用も担当しています。 そのため、私達のグループはもとより、他の分析者の生産性を上げることに責任を持っていかないといけません。 今回はDREグループで始まったfourkeysによる生産性の計測と今後の展望について書かせていただこうと思います。 前提としてfourkeys 今回の計測に当たってはfourkeysをベースとして考えることにしました。 これについては書籍や色々な発表資料などを漁り、グループ内で勉強会などを通してある程度共通理解を得て我々の計りたいものに対してまずはこれでいってみよう、という合意が得られたためです。 我々が考える生産性については後述しますが、前提としてのfourkeysは以下の本や資料などを読んでいただけると理解が進むかと思います。 State of DevOps 2021 https://services.google.com/fh/files/misc/state-of-devops-2021.pdf (2022もありますが、フォーム入力が必要なためパブリックに公開されているこちらを貼っています)  エリート DevOps チームであることを Four Keys プロジェクトで確認する https://cloud.google.com/blog/ja/products/gcp/using-the-four-keys-to-measure-your-devops-performance 継続的デリバリーのソフトウェア工学 https://bookplus.nikkei.com/atcl/catalog/22/12/01/00531/ DREグループの考える生産性の定義 我々DREグループは社内向けのデータ基盤を提供するグループです。 つまり、生産性計測の対象が1つのチームに閉じるとは限らず、何なら複数グループの活動が全て終わって1つの機能提供が完了するというケースも多くあります。 例えば、新規データをつなぎ込みモデリングした上でダッシュボードを提供しようとするケースでは、DREグループだけでなく、モデリングを担当するBIグループが絡んできます。このとき、DREグループが担当するステージング処理までが完了したとしても価値が提供できていると言えるでしょうか?そうではなく、データの取り込み、ステージング処理、モデリング、ダッシュボード化のすべてが完了して価値を提供できていると言えると考えています。 そのため、開発はもちろんですが、その2グループの連携がスムーズに流れることも重要な要素であり、その点も改善の対象となってくるはずです。例えば、もしドキュメントが足りなくてBIグループがモデリングに入れないのであればそれをスムーズに提供するべきだし、依頼内容が曖昧でヒアリングに時間がかかりすぎるならそこも改善対象かもしれません。 つまり、スループットとしては絵にすると以下の全体で計測・改善を行う必要があると考えています。 また、単に速度だけを追求してしまうと障害を起こしまくる仕組みになってしまいかねません。 そのため、同時に安定性の指標として障害も計測する必要があるのですが、データ基盤の場合シンプルにデプロイによる失敗を計測すればよいかというとそういうことでもなく、どうしてもデータソース側の変更などにより「何もしてないのに壊れた」ということが発生することも多くあります。 そのあたりを考慮して実装を考えていく必要があります。 実装 実装は以下の様になりました。 基本的にDORAが公開してくれている実装を使い、Github上の営みをBigQuery上に収集しています。 デプロイ周りについては、 ほとんどのリポジトリはmain merge時にデプロイ自動化されているので、マージをデプロイの代替指標として採用する 障害は現在Notion上のポストモーテムDBで管理できているのでそこから引っ張ってくる ということにしています。 現時点では通常のfourkeysを取得できるようにしていますが、今後は上述した通りのリードタイムをNotion上にあるバックログなどと連携させて取得していく予定です。 考察と今後について 現時点ではほぼ素のfourkeysが取得できた状態です。 最初に計りたかったものからするとやや乖離がある状態ですが、それでもいくつかの示唆を得ることができました。 例えば、dbtのリポジトリでDREグループのリリース速度は十分早いのですが、データのモデリングを行うBIグループはかなりの時間がかかっておりなにか課題がありそうなことが特定できました。 これについてはヒアリングを行っており、実際にメンバー間でモデリングについての習熟度や前提条件の理解などの差があるために、レビューで大きな時間がかかっていることがわかりました。 また、当たり前ですがDREグループだとしても意図して大きいPRを出すとマージまでの時間が圧倒的に伸びていることがわかります。 どちらも担当ベースでは理解のあることと思いますが、具体的な数値として見えてくると議論の俎上にも上がりやすく、解決に向けて動きやすくなるのでシンプルなfour keysでも意味のある可視化なのではないかと思います。 ただ、やはりこれでは最初に書いたような課題は見えてこない部分はあるので、少しずつアップデートしていき、エンドツーエンドのfourkeysを測れるようにしてより生産性の高いデータ基盤を実現する礎にしていければと思っており、そのあたりはまたブログ等で発表できればと思っています。 おわりに 我々DREグループはデータ基盤を提供する裏方の部門ではありますが、データを使った業務の生産性を最大化するために業務に取り組んでいます。 こういった話に興味がある、少しでも気になった方はお気軽に カジュアル面談 に申し込みいただけると嬉しいです。
アバター
こんにちは、マッチング領域でバックエンドエンジニアをしているぽこひで ( @pokohide ) です。 冷やし中華はじめました的なタイトルですね。分かります。 今回はタイミーが本番運用しているRailsアプリケーションに対してRails edgeでCIを回すようになった話を紹介します。翌週には「〜見つけたエラー編(仮)〜」と題して、実際に弊社で見つけたエラーの例を紹介していきます。記事公開時点(2023年7月)のバージョンは下記の通りです。 $ ruby -v ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [aarch64-linux] $ rails -v Rails 7.0.6 弊社ではRubyもRailsも積極的に最新バージョンにあげる活動をしています。今回の記事はRailsに関してですが、Rubyのアップグレードも同様に行っています。過去にはRuby3.2にし、YJITを有効化にした記事を公開しているので興味があれば、ご一読ください。 tech.timee.co.jp Rails edgeとは? ChatGPTに聞いて楽をしてみました。 この記事ではChatGPTと同様に rails/rails のmainブランチを指すこととします。 Rails edgeは、安定版ではなく開発版なので以下のような特徴があります。 将来のリリースで利用可能になるかもしれない新機能が含まれている 正式リリースされていない既知のバグ修正が含まれている 安定版ではなく、まだ評価されていない機能やバグが含まれている可能性があるため、本番環境で使用する際には注意が必要です。GitHubでは、毎週本番環境で使うRailsをedgeにアップデートしているそうです *1 。 なぜRails edgeでCIを回すのか タイミーでは、RubyやRailsのコミュニティの進化に継続的に追随することで、高速化や機能追加などの恩恵を受け、ユーザーに最大限の価値を提供していきたいと考えています。 Rails edgeでは、将来のリリースで利用可能になる新機能を事前に検査できるため、潜在的な影響を確認することができます。また、バグや互換性の問題を早期に発見し、解決策を見つけることも可能です。その他にも、最新の機能や修正に関して何かあれば、安定版リリース前に異議申し立てをしやすいといったメリットもあります。 これらの利点を考慮し、Rails edgeでCIを回すことにしました。 Rails edgeでCIを回す rails以外のgemはそのまま利用したいため、既存のGemfileを読み込み、railsのみを rails/rails に上書きすることでRails edge用のGemfileを用意します。 # frozen_string_literal: true # 既存のGemfileを読み込む eval_gemfile File .expand_path( ' ../Gemfile ' , __dir__ ) # 上書きしたい依存関係を削除する dependencies.delete_if { |d| d.name == ' rails ' } # rails/railsのメインブランチに上書きする gem ' rails ' , branch : ' main ' , github : ' rails/rails ' 使用するgemのバージョンが変更されたので専用のlockファイルを用意する必要があります。 BUNDLE_GEMFILE を指定する事で指定したGemfileを使用できます。 $ BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install これによりRails edgeと他のGemとの依存関係を定義したファイルの用意が完了します。次にこれらのファイルを利用してCIでテストを実行する準備をしていきます。実際の設定ファイルを簡略化して紹介しています。なお、弊社ではCIツールとしてCircleCIを利用しています。 version : 2.1 orbs : ruby : circleci/ruby@2.0.1 jobs : test : parameters : gemfile : type : string default : Gemfile environment : BUNDLE_GEMFILE : << parameters.gemfile >> BUNDLE_PATH_RELATIVE_TO_CWD : true docker : - image : cimg/ruby:3.2.2 steps : - checkout - ruby/install-deps : key : gems-<< parameters.gemfile >> gemfile : << parameters.gemfile >> - run : name : Test command : bundle exec rspec workflows : version : 2 workflow : jobs : - test : filters : branches : only : - master - /^rails_edge.*$/ matrix : parameters : gemfile : - "gemfiles/rails_edge.gemfile" 今回はmasterブランチと rails_edge.* ブランチでのみ実行するようにしました。 Orb - circleci/ruby で定義されるコマンドである install-deps ではgemfileパラメータを渡すと指定したGemfileを利用してbundle installを実行してくれるため、その機構を利用しています。 また、 BUNDLE_PATH_RELATIVE_TO_CWD を利用することで、Gemfileではなくカレントディレクトリに対して相対的にパスを指定するのでGemの競合を回避でき、キャッシュを有効活用できます。 Rails edgeで落ちるテストに印をつける 後出しですが今回の目的は「 Rails edgeでCIを回す 」ことであり、全てのテストを修正することではありませんでした。そこで、今回は落ちたテストをpendingすることにしました。 skipではなくpendingを採用したのは、mainブランチの追従によりテストが成功する可能性があるためです。skipはテストの成功・失敗に限らずテストを実行しませんが、pendingはテストの失敗時のみ保留にするため修正に気づくことができます。 便宜上、これから出るであろうRails7.1で動かないこととそれまでに直すぞという意味合いも込めて以下のようなメッセージを表示するようにしています。ここは適当に変えていただくのが良いかと思います。 # frozen_string_literal: true module PendingIfRailsEdge def pending_if_rails_edge ' PENDING: Rails 7.1で動くように直す ' if Rails :: VERSION :: STRING .start_with?( ' 7.1 ' ) end end RSpec .configure do |config| config.extend PendingIfRailsEdge end 以下のように使えます。こうすることでテストコードをgrepした時に落ちているコードをすぐに見つけることができるの便利でもあります。 context ' foo ' , pending : pending_if_rails_edge do ... end it ' bar ' , pending : pending_if_rails_edge do ... end これでRails edgeでCIを回すことができるようになりました。 Rails edge用のlockファイルを追従させる masterブランチと rails_edge.* ブランチでRails edgeでCIを回す運用を開始してから、featureブランチでGemの追加や削除を行ったが rails_edge.gemfile の更新を忘れてmasterブランチにマージしたため、masterブランチでRails edgeでのCIがfailするケースが発生しました。 本番環境に影響がないとはいえmasterブランチのCIがfailしているのはモヤモヤすることや、せっかく導入した仕組みが形骸化してしまうため、masterブランチマージ前に気づける仕組みを導入しました。 具体的には BUNDLE_GEMFILE=gemfiles/rails_edge.gemfile bundle install を実行して差分が発生すれば失敗するステップを追加し、featureブランチでも実行するようにしました。 jobs : check_outdated_gemfile : parameters : gemfile : type : string docker : - image : cimg/ruby:3.2.2 steps : - checkout - ruby/install-deps : key : gems-<< parameters.gemfile >> gemfile : << parameters.gemfile >> - run : name : re-bundle install command : bundle install - run : name : check file changes command : | if [ `git diff --name-only << parameters.gemfile >>.lock` ] ; then echo 'Please run `BUNDLE_GEMFILE=<< parameters.gemfile >> bundle install`' exit 1 else exit 0 fi 最後に 今回はRails edgeでCIを回し始めた背景や導入する方法について紹介しました。 今回の取り組みを通して、将来のRailsアップグレードにおいて遭遇するであろうバグに早期に気づき、迅速な対応ができるようになりました。まだGitHubのように *1 、 Rails edgeを毎週取り込める状態ではないですが、引き続き頑張っていこうと思います。 次回は、Rails edgeでCI回し始めたことで見つけた問題を紹介していきます。記事公開時には公式Twitter ( @TimeeDev ) でアナウンスしていくのでフォローしていただけると嬉しいです。 また、タイミーでは一緒にサービスを成長させていく方を募集しています。もし少しでも興味を持っていただけたら、カジュアル面談受け付けておりますので是非お話ししましょう! product-recruit.timee.co.jp *1 : https://github.blog/2023-04-06-building-github-with-ruby-and-rails
アバター
こんにちは、タイミーのデータ統括部データサイエンス(以下DS)グループ所属の 小栗 です。 今回は、DSグループのメンバーにおすすめの本を聞いてみたのでご紹介します! *1 おすすめ本を通して、DSグループの雰囲気や、業務で活用するスキル・知識について、みなさんに伝わればいいなと考えています。 データサイエンス(DS)グループの紹介 本題に入る前に、軽くDSグループの紹介をさせてください。 DSグループは、タイミーの事業成長をデータ・アルゴリズムで支援することを目的としています。 例えば、以下のような業務を日々行なっています。 Google Cloudを利用した機械学習パイプライン&基盤の開発・運用 ユーザーへ仕事を推薦するレコメンドエンジンの開発 営業活動を支援する予測モデルの開発 ビジネス施策の効果検証 現在、専任・兼任・業務委託のメンバーで構成されており、今後も規模を拡大する予定です。 Web企業、コンサル企業、AIベンチャーなどで、データサイエンティスト/機械学習エンジニアとして経験を積んできたメンバーが在籍しています。 特に、機械学習システム設計、機械学習モデル実装、効果検証などに強みがあるメンバーが揃っています。 DSグループおすすめ本の紹介 それでは、DSグループの各メンバーに聞いた「心からおすすめできる本」を、推薦メンバーからのコメントを載せつつ紹介します。 ジャンルとしては、機械学習、因果推論、開発、組織に関する書籍を取り上げます。 機械学習のおすすめ本 Rによる統計的学習入門 Rによる 統計的学習入門 作者: Gareth James , Daniela Witten , Trevor Hastie , Robert Tibshirani 朝倉書店 Amazon メンバーからのコメント↓ カステラ本として有名な 『統計的学習の基礎』 を手掛けた著者が書いた、機械学習の入門書。 『統計的学習の基礎』は良書ではあるものの内容・分量がヘビーなので、実務家に一番最初におすすめしたいのはコレかなと思ってます。 説明の平易さが適切であり、個人的に入門書の中で一番わかりやすかったです。 ベイズ推論による機械学習入門 機械学習スタートアップシリーズ ベイズ推論による機械学習入門 作者: 須山敦志 講談社 Amazon メンバーからのコメント↓ ベイズ推論による学習・予測を丁寧に数式レベルで追うことができる良書です。 ギブスサンプリングや変分推論を理論から理解したい方におすすめしたいです。 自然言語処理の基礎 IT Text 自然言語処理の基礎 作者: 岡﨑直観 , 荒瀬由紀 , 鈴木潤 , 鶴岡慶雅 , 宮尾祐介 オーム社 Amazon メンバーからのコメント↓ 自然言語処理の基礎から始まり、近年の深層学習ベースの手法まで丁寧に解説した本。 Transformer、BERT、GPTなど、大規模言語モデルの興隆を支える新しい技術の解説もあり、和書としては貴重です。 推薦システム実践入門 推薦システム実践入門 ―仕事で使える導入ガイド 作者: 風間 正弘 , 飯塚 洸二郎 , 松村 優也 オライリージャパン Amazon メンバーからのコメント↓ 推薦システムについて、アルゴリズムの解説に留まらず、企画段階やデザインまで幅広く扱った実践的な一冊です。 本書を教材にDSグループで勉強会を開くなど、実務で重宝しました! 因果推論のおすすめ本 効果検証入門 効果検証入門〜正しい比較のための因果推論/計量経済学の基礎 作者: 安井 翔太 技術評論社 Amazon メンバーからのコメント↓ RCTだけでなく、観察データを用いた因果推論の手法をわかりやすく解説した、実務家向けの一冊。 DSグループではビジネス施策の効果検証を行うことも多く、仕事をする上で基礎になっています。 DSグループと同じ部署に属するBIグループも勉強会で本書を使っていたので、一緒にわいわい学びました。 統計的因果推論の理論と実装 統計的因果推論の理論と実装 Wonderful R 作者: 高橋将宜 , 石田基広 , 市川太祐 , 高橋康介 , 高柳慎一 , 福島真太朗 , 松浦健太郎 共立出版 Amazon メンバーからのコメント↓ 図表を交えた解説がわかりやすい、理論と実装のバランスが良いなど、非常に洗練された書籍です。 『効果検証入門』では深く説明されなかった手法も解説されており、併せて読むのがおすすめです。 開発系のおすすめ本 ロバストPython ロバストPython ―クリーンで保守しやすいコードを書く 作者: Patrick Viafore オーム社 Amazon メンバーからのコメント↓ Pythonで保守しやすい、堅牢なコードを書くための情報がよくまとまっています。 機械学習モデリングやデータ処理を含む実験コードを本番環境に乗せていく際に、本書や 『リーダブルコード』 に立ち返って開発やレビューすることを意識しています。 単体テストの考え方/使い方 単体テストの考え方/使い方 作者: Vladimir Khorikov マイナビ出版 Amazon メンバーからのコメント↓ 単体テストの考え方を網羅的かつ深くまとめた書籍。 DSグループでは(アドホック分析のコード以外は)テストを実装することにしているため、価値のあるテストケースをつくるため手元に置いている一冊です。 データマネジメントが30分でわかる本 データマネジメントが30分でわかる本 作者: ゆずたそ , はせりょ , 株式会社風音屋 Amazon メンバーからのコメント↓ 中身も見た目もヘビーすぎる DMBOK を独自に要約してまとめた本。 DMBOKの本質が簡潔にまとめられており、(大半のケースでは)こちらを参照することで問題を解決できる気がしています。 ソフトウェア見積り 人月の暗黙知を解き明かす ソフトウェア見積り 人月の暗黙知を解き明かす 作者: スティーブ マコネル 日経BP Amazon メンバーからのコメント↓ 有名な「不確実性コーン(プロジェクトの進行に伴って不確実性が減少することを表した図)」の初出本。 ソフトウェア開発における見積もりに関して、本質的な視点を提供してくれます。 開発に関わるすべての人におすすめできる書籍です。 組織系のおすすめ本 エンジニアリング組織論への招待 エンジニアリング組織論への招待 ~不確実性に向き合う思考と組織のリファクタリング 作者: 広木 大地 技術評論社 Amazon メンバーからのコメント↓ エンジニアリング組織における課題の解決方法についてまとめられた本。 アジャイル的な思考や、不確実性の考え方はエンジニアだけでなくデータサイエンティストにとっても重要なので、おすすめです。 恐れのない組織 恐れのない組織――「心理的安全性」が学習・イノベーション・成長をもたらす 作者: エイミー・C・エドモンドソン , 村瀬俊朗 英治出版 Amazon メンバーからのコメント↓ 「心理的安全性」の提唱者であるエドモンドソン博士が、心理的安全性が組織にもたらす影響についてまとめた本。 関連書籍は今たくさんあるのですが、これ一冊読めば心理的安全性のコアがわかり、応用が効くと思っています。 データ統括部では 心理的安全性勉強会 を開催するなど、文化の浸透をはかっています。 We’re Hiring! タイミーのデータ統括部では、こういったおすすめ本の情報が日々Slackに飛び交っていたり、勉強会を定期的に行なっていたりします。 学習意欲や好奇心のある人にとって嬉しい環境ではないかと、(手前味噌ながら)いちメンバーとして思っています。 タイミーでは、データサイエンティストやエンジニアをはじめ、一緒に働くメンバーを募集しています! product-recruit.timee.co.jp *1 : 先日、データ統括部メンバーのおすすめ本も note で紹介しましたので、興味があればご一読ください。
アバター
はじめに 初めまして、タイミーのDREチーム(Data Reliability Engineering Team)でエンジニアをしてます、筑紫です。 今回DREチームで実施した合宿ついてご紹介させて頂こうと思います。 DREのカルチャーを少しでも知って頂けたら嬉しいです。 DREチームについて紹介 DREチームでは、社内の様々データを集約し、クレンジングを行い、社内外で利活用できる形で提供するためのデータ基盤プロダクトの開発・運用を行なっております。 データ基盤の詳細については、ProductOwner(以降POと記載)の土川の記事をご参照ください。 tech.timee.co.jp 今年の4月に私を含め2人入社したことでメンバーの入れ変えもあり、データ基盤の開発体制が新しくなりました。 メンバーが大きく変わったこともあり、開発を進める上で今までのデータ基盤の歴史的背景や方向性の理解にメンバー間で差分があることが課題になっていました。 DREチームのMissionと合宿の目的 DREではチームとしてのMissionを定めています。 ここでいうMissionとは、ビジネスシーンでよく用いられる組織やチームのMVV(Mission・Vision・Value)のMissionで、組織やチームの存在意義、果たすべき使命を指します。 Missionを定めて、チームとしての明確な目標をチームの共通言語にすることで、チームの役割や責任範囲を明確化し、成果の向上や成功につながるデータ基盤プロダクトの開発を効率的に進めることができます。 また、Missionは外部とのコミュニケーションをスムーズにし、チームのモチベーションを高める要素ともなります。 元々のMissionは以下の通りです。 『信頼性の高いデータ基盤を整備し、活用のための環境を提供する』 ただ、Missionを定義した頃から時間も経っていること、またチーム構成が大きく変わったこともあり、アップデートしたい機運が高まっていました。 今のチームでMissionを再検討することで、上述の課題も含め解消できるのではという話になり、DREチームのMissionを決めるワークショップを開催することになりました。 合宿の内容 普段はリモートワークが多いチームですが、合宿は1日だけ東京オフィスに集まり、会議室を貸し切って実施することになりました。 Missionを決めるにあたって、まずは、DREチームの存在意義についてメンバーそれぞれポストイットで意見を出し合い、それを議論しながらクラスタリングしました。 その結果を基に、Missionの方向性を導き出す形で進めました。 ただ、ポストイットの結果からMissionという形で、抽象度高いフレーズを抽出することに苦戦しました。 重要視する要素については、メンバー間で認識が概ね揃っていたものの最終的にMissionという形でどう表現するかに難儀しました。 議論中で、元々のMissionに入っていた、”信頼性”というワードを採用することになりました。 タイミーのDREチームにおける“信頼性”とは、スピード、品質、安定性、ユーザビリティを総合的に表現したものであり、ユーザがデータを利活用する上で、DREチームではこれらの要素を特に重視しています。 特にスピードについては、データが生成されてから活用されるまでの時間を短縮していきたいというPOの思いがあり、また、これからリアルタイム性を求められる要求に対応していくためにも温度感の高い指標になってきています。 また、高い”信頼性”を達成するための手段として、DataOpsという観点を導入することになりました。 DataOpsは、ガートナー社が提唱している概念で、データパイプラインの構築、自動化、監視、デプロイメントの迅速化、データ品質の向上などを重視することで、組織のデータ管理者と利用者の間の連携促進し、データの収集、処理、分析、展開のプロセスを効率化するためのプラクティスです。 このプラクティスを用いて、”信頼性”あるデータ基盤を構築していくことをMissionとすることになりました。 その後議論が進み、最終的に以下のMissionに決まりました。 『DataOpsを実現し、信頼性の高いデータ基盤プロダクトを提供する』 まとめ 日頃このようなチームの方向性などを深く話を機会が少ないので、とても貴重な時間を過ごせました。 チーム内でMissionを定めることができ、同じ方向を向いてプロダクト開発を進めていけそうで、メンバー間での満足度も高く、良かったと思います。 また、それ以上にチームで議論しながら、データ基盤プロダクトの構想や方向性をPO+開発メンバー間で共有できたという、その過程にとても価値がある会だったと思います。 今回定めたMissionを持ってデータ基盤プロダクトの成長を加速させていきたいと思ってます。 最後に、タイミーではエンジニア・データサイエンティストを初め、様々な職種のメンバーを募集してます! product-recruit.timee.co.jp
アバター
こんにちは。2023年1月に株式会社タイミーに入社したバックエンドエンジニアの id:euglena1215 です。 RubyKaigi 2023 がとうとう明日に迫ってきました。楽しみですね。 タイミーは RubyKaigi で初めてブース出展を行います。至らぬ点もあるかと思いますが、RubyKaigi を一緒に盛り上げていければと思っています!どうぞよろしくお願いします。 今回はタイミーが本番運用している Rails アプリケーションに対して Ruby 3.2.2 へのアップデートと YJIT の有効化を行い、パフォーマンスが大きく改善したことを紹介します。 RubyKaigi で「Ruby 3.2+YJIT 本番運用カンパニーです」と言いたいので粛々と進めている — Shintani Teppei (@euglena1215) 2023年4月19日 前提 タイミーを支えるバックエンドの Web API は多くのケースで Ruby の実行よりも DB がボトルネックの一般的な Rails アプリケーションです。JSON への serialize は active_model_serializers を利用しています。 今回の集計では API リクエストへのパフォーマンス影響のみを集計し、Sidekiq, Rake タスクといった非同期で実行される処理は集計の対象外としています。 今回は Ruby 3.1.2 から Ruby 3.2.2 へのアップデートと YJIT 有効化を同時に行いパフォーマンスの変化を確認しました。そのため、パフォーマンスの変化には Ruby のバージョンアップによる最適化と YJIT 有効化による最適化の両方の影響があると考えられますがご容赦ください。 結果 以下のグラフは API リクエスト全体のレスポンスタイムの 50-percentile です。アップデート前後でレスポンスタイムが 約10%高速化されている ことが確認できました。 API リクエスト全体のレスポンスタイムの 50-percentile リクエスト全体としては大きく高速化されていることが確認できました。それでは、レスポンスが遅く、時間当たりのリクエスト数が多いアプリケーションの負荷の多く占めるエンドポイントではどうでしょうか? そこで、タイミーの Web API のうち2番目に合計の処理時間 *1 が長いエンドポイントへのパフォーマンス影響を確認しました。 *2 以下のグラフは2番目に合計の処理時間が長いエンドポイントのレスポンスタイムの 50-percentile です。Ruby アップデートの数日前に GW 中の負荷対策のためのスケールアウトを実施したことで変化が少し分かりにくくなっていますが、Ruby 3.1.2 から Ruby 3.2.2+YJIT にしたことによって 10%以上高速化されている ことが確認できました。 2番目に合計の処理時間が長いエンドポイントのレスポンスタイムの 50-percentile 元々十分に高速なエンドポイントだけでなく、アプリケーション負荷の多くを占めていたエンドポイントのパフォーマンスも改善されていることが分かります。Ruby 3.2 アップデート+YJIT 有効化はパフォーマンスチューニングへの十分実用的な打ち手と言えるのではないでしょうか。 まとめ Ruby 3.1.2 から Ruby 3.2.2 へのアップデートと YJIT を有効にしたことでリクエスト全体のレスポンスタイムの 50-percentile が約10%高速化されました。また、アプリケーション負荷の多くを占めていたエンドポイントも同様に10%以上高速化されていることを確認できました。 Ruby 3.2’s YJIT is Production-Ready でも YJIT によって Shopify が 5~10% 高速化されたと記されていることから、「Ruby 3.2 の YJIT は一般的な Rails アプリケーションを 10%程度高速化させる」と考えて良いのではないかと思っています。 これほどの高速化に尽力していただいた Ruby コミッターのみなさん、本当にありがとうございました。 余談ではありますが、自社で YJIT を有効化したことによって YJIT という技術がより自分ごとになり、内部で何が行われているのかをきちんと理解したいと思うようになりました。 RubyKaigi 2023 で理解を深めようと思います。 宣伝 冒頭で説明したように、タイミーは RubyKaigi 2023 でブース出展を行います。今回の記事ではパフォーマンス改善の結果のみ紹介しましたが、タイミーでのこういった技術改善における取り組み方など話したいことは色々あるので、ぜひブースでお話しさせてください! また、RubyKaigi 後 5/16(火)にはスポンサー振り返り会を Qiita さん、Wantedly さんと実施予定です。 「自分の会社も RubyKaigi スポンサーをしてほしいと思っているエンジニア」をターゲットにしたちょっとニッチな会ですが、もしかして自分ターゲットかも…?と思う方はぜひご参加ください! wantedly.connpass.com *1 : 合計の処理時間 = 平均レスポンスタイム x hits数 *2 : 最も合計の処理時間が長いエンドポイントはGWの繁閑の影響でレスポンスタイムに変化が生じ、比較が困難であったため除外しています。
アバター
こんにちは。2022年11月に株式会社タイミーに入社した sinsoku です。 最近は GitHub Actionsの YAML を書く機会が多く、 YAML も複雑化してきました。 しかし、日常的に YAML を触っている職人以外にはパッと読めないことも多いので、社内の方々が読めるように GitHub Actionsの YAML の書き方をまとめたいと思います。 目次 三項演算子 環境変数(env) 変数(outputs) 関数(workflow_call) 関数 + 配列(dynamic matrix) GitHub CLIの活用 まとめ 三項演算子 GitHub Actions には 三項演算子 がないため、代わりに論理 演算子 を使います。 - steps : - run : echo "${{ (github.ref == 'refs/heads/main' && 'production') || 'staging' }}" 参考: Expressions 環境変数 (env) 環境変数 を使いたい場合は env で定義します。 env : TIMEE_CEO : ryo TIMEE_CTO : kameike jobs : job-env : runs-on : ubuntu-latest steps : - run : echo $TIMEE_CEO # この実装だと置換後の文字列 `echo kameike` を実行する - run : echo ${{ env.TIMEE_CTO }} ただし、 env で env の値を指定するとエラーになるケースがあるので注意してください。 env : DEPLOY_ENV : ${{ (github.ref == 'refs/heads/main' && 'production' ) || 'staging' }} # Unrecognized named-value: 'env'. Located at position 1 within expression: env.DEPLOY_ENV == 'production' IS_PROD : ${{ env.DEPLOY_ENV == 'production' }} jobs.<job_id>.env でも同様のエラーが出ます。 jobs : job-error : runs-on : ubuntu-latest # Unrecognized named-value: 'env'. Located at position 1 within expression: env.DEPLOY_ENV == 'production' IS_PROD : ${{ env.DEPLOY_ENV == 'production' }} jobs.<job_id>.steps[*].env であればエラーになりませんが、同じ階層の env の値は参照できません。 env : TIMEE_CTO : kameike jobs : job-env : runs-on : ubuntu-latest steps : # この実装だと `echo "true, foo, -bar"` を実行する - run : echo "${{ env.IS_KAMEIKE }}, ${{ env.FOO }}, ${{ env.BAR }}" env : IS_KAMEIKE : ${{ env.TIMEE_CTO == 'kameike' }} FOO : foo BAR : ${{ env.FOO }}-bar 参考: Workflow syntax for GitHub Actions 変数(outputs) 汎用的な名前の 環境変数 を定義すると、何かの CLI コマンドに影響する可能性があります。 これを避けるために、outputs で変数を定義することもできます。 steps : - id : var run : | echo "x=foo" >> "$GITHUB_OUTPUT" echo "y=bar" >> "$GITHUB_OUTPUT" # この実装だと `echo "foo, bar"` を実行する - run : echo "${{ steps.var.outputs.x }}, ${{ steps.var.outputs.y }}" outputs は env と違い、 bash の処理結果を変数に定義することができます。 steps : - uses : actions/checkout@v3 - id : var run : | echo "terraform-version=`cat .terraform-version`" >> "$GITHUB_OUTPUT" # この実装だと `echo "1.4.4"` を実行する - run : echo "${{ steps.var.outputs.terraform-version }}" 参考: Defining outputs for jobs 関数(workflow_call) ワークフローの一部を別のファイルに定義し、関数のように呼び出すことができます。 # .github/workflows/_say.yml name : say on : workflow_call : inputs : name : required : true type : string jobs : hello : runs-on : ubuntu-latest steps : - run : echo "Hello, ${{ inputs.name }}." bye : needs : hello runs-on : ubuntu-latest steps : - run : echo "Bye, ${{ inputs.name }}." 使い方は以下の通りです。 jobs : job-say : uses : ./.github/workflows/_say.yml with : name : kameike 参考: Reusing workflows 関数 + 配列(dynamic matrix) workflows_call の入力には真偽値、数字、文字列しか使えません。 The value of this parameter is a string specifying the data type of the input. This must be one of: boolean, number, or string. 引用: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_callinputsinput_idtype しかし、少し工夫することで配列を扱うことができます。 # .github/workflows/_say_multi.yml name : say_multi on : workflow_call : inputs : names : required : true type : string jobs : matrix : runs-on : ubuntu-latest outputs : names : ${{ steps.set-matrix.outputs.names }} steps : - id : set-matrix run : | names=`echo '${{ inputs.names }}' | jq -csR 'split("\\n") | map(select(. != ""))' ` echo "names=$names" >> $GITHUB_OUTPUT hello : needs : matrix runs-on : ubuntu-latest strategy : matrix : name : ${{ fromJSON(needs.matrix.outputs.names) }} steps : - run : echo "Hello, ${{ matrix.name }}." 使い方は以下の通りです。 job-say-multi : uses : ./.github/workflows/_say_multi.yml with : names : | ryo kameike GitHub CLI の活用 GitHub API を使うことで、チェックアウトせずにファイル名を取得することができます。 job-gh-matrix : runs-on : ubuntu-latest outputs : files : ${{ steps.set-matrix.outputs.files }} steps : - id : set-matrix run : echo "files=`gh api '/repos/{owner}/{repo}/contents/.github/workflows?ref=${{ github.sha }}' --jq '[.[].name]'`" >> $GITHUB_OUTPUT env : GH_REPO : ${{ github.repository }} GH_TOKEN : ${{ github.token }} job-gh-echo : needs : job-gh-matrix runs-on : ubuntu-latest strategy : matrix : file : ${{ fromJSON(needs.job-gh-matrix.outputs.files) }} steps : - run : echo "${{ matrix.file }}" 画像のようにファイル名の一覧で並列にJobを実行できます。 まとめ YAML むずかしいですね。 この記事が読んだ方の参考になれば幸いです。 また、弊社の GitHub Actionsやデプロイフローについて気になることがあれば、フッターの採用ページのURLから面談の申し込みをどうぞ!
アバター
こんにちは、フロントエンドエンジニアの樫福です。 タイミーのフロントエンドの開発に関わるエンジニアの人数が増えてきました。大人数で開発しながら品質を高い状態に保つには、品質に対する共通認識を作ることが大切です。 このたび、チームでフロイントエンドの 単体テスト についての勉強会を開催しました。 testing-library というフロントエンドのテストに使うライブラリを例に挙げ、具体的な手法よりも、テストを実装する前に抑えておきたい思想についてフォーカスしました。 フロントエンドでテストしたい項目 フロントエンドの単体テストを難しくする要因 testing-library を使って壊れにくいテストを作る方法 要素を見つけるクエリ ユーザの動作をシミュレーションする user-event 要素の状態を検査する jest-dom 実際にテストを書いてみる テストが書けないケース 運用するときの注意点 単体テストを作る目的を明確にする アクセシビリティのガイドラインを定める おわりに フロントエンドでテストしたい項目 フロントエンドでテストしたい項目には次のようなものがあると思います。 ユーティリティ関数や ビジネスロジック が正しく実装されていること 意図しない見た目の変更が起こらないこと ユーザの動作に対して期待した正しい応答が返ってくること ユーティリティ関数や ビジネスロジック は関数やクラスとして切り出し、それらに対しての 単体テスト を書くことになります。基本的には入力に対する出力を確認する出力値ベーステストでテストします。クラスで実装する場合にはアクションに伴って変更した状態を検査する状態ベーステストも併せて使います。これらはフロントエンド以外でも扱うトピックです。 意図しない見た目の変更が起こらないことをテストする場合は Visual Regression Test (VRT) を使います。 reg-suit などのツールを使って、変更ごとの画像のキャプチャを撮って比較します。 ユーザがボタンをクリックする、などの動作のテストは 単体テスト で実装することができます。 testing-library というライブラリを使ってユーザの動作をシミュレーションすることで、 UI コンポーネント の挙動を検査します。画面上の表示に対しては状態ベーステストを、 API にリク エス トを送ることのテストはコミュニケーションベーステストを実装することになります。 テストトロフィーとそれぞれのテストで検査できること *1 勉強会では 単体テスト におけるフロントエンド特有の難しさについて扱いたいので、3つ目のような「ユーザの画面上に表示された要素に対する動作を起点とするようなテスト」に注目しました。 フロントエンドの 単体テスト を難しくする要因 一般的なソフトウェアと比較してフロントエンドにおいて特筆すべき点に次のようなものがあります。 ユーザが要素を取得する ユーザが(マウスやキーボードの操作などの)アクションを実行する 要素が変化したことを検査する ユーザのアクションなどに応じて API リク エス トをする フロントエンドのテストが壊れやすかったり理解するのが難しくなったりする場合、とくに、要素の取得をシミュレートすることに躓いていることが多いように感じています。 testing-library を使って壊れにくいテストを作る方法 testing-library はフロントエンドのテストに使うツール群です。 testing-library には次のような基本方針があります。 The more your tests resemble the way your software is used, the more confidence they can give you. testing-library.com そのソフトウェアが実際に使われる姿に似ているほど、テストの信頼性が高くなります。 testing-library は、ソフトウェアの使われ方に似たテストを作れるような機能を提供してくれています。ライブラリを使ってテストを実装するときには、この考え方に則ってテストを書くように心がけましょう。 以下では、 testing-library が提供している機能を3つ紹介します。 要素を見つけるクエリ 一つ目は、ページに表示されている要素を見つけるクエリです。要素に対して動作をする場合も、要素の状態を検査する場合も、まずは要素を見つけることから始まります。 testing-library.com クエリには優先度があり、なるべく優先度が高いものを使うことが推奨されています。たとえば、 getByRole は優先度が高く、 getByPlaceholderText は優先度が低いです。これらはどのように決められているのでしょうか。 たとえば、ラベルが『生年月日』で プレースホルダ ーが "2023-03-27" であるような入力要素を見つけるクエリを作ってみます。次の2種類の実装のいずれも想定通りに動きました。 // 1. ラベルが『生年月日』である入力要素を取得する screen.getByRole ( "textbox" , { name: "生年月日" } ); // 2. プレースホルダーが "2023-03-27" であるような入力要素を見つける screen.getByPlaceholderText ( "2023-03-27" ); 1, のクエリは「ラベルが『生年月日』である入力要素」を取得しています。2. のクエリは「 プレースホルダ ーが "2023-03-27" であるような入力要素」を取得しています。ユーザがアプリケーションを使用する際、おそらく 1. のクエリのような考え方で要素を認識するでしょう。 getByRole が getByPlaceholderText よりも優先度が高い理由は、このようなユーザの考え方を反映しやすい傾向にあるからです。 必ずしも優先度が高いクエリを使うことが良いわけではないですが、なるべくユーザの考え方を反映させて要素を取得するクエリを書くことはよいテストを作りに欠かせないと思います。 ユーザの動作をシミュレーションする user-event ユーザの動作をシミュレーションするために user-event というライブラリを提供しています。 testing-library.com 特定の要素をクリックしたり、キーボードで入力したりをシミュレーションする際は次のように実装します。 // ユーザオブジェクトの生成 const user = userEvent.setup (); // ボタンをクリックする await user.click ( screen.getByRole ( "button" )); // 入力要素に「こんにちは」と入力する await user. type( screen.getByRole ( "textbox" ), "こんにちは" ); user.click を実行すると、実際に要素をクリックしたときと同じようにイベントが発火します。これによって、実装者はボタンをクリックしたときに裏側でどのような挙動が取られるかを気にすることなくテストを実装することができます。 同じようにユーザの動作をシミュレーションする方法に、同じく testing-library の fire-event を使った実装があります。こちらは、ユーザの動作そのものではなく DOM のイベントを発火させる機能を持っています。 user-event と fireEvent ではどちらを使うべきでしょうか。 ユーザがアプリケーションを使う場合、「このボタン要素の click イベントを発火させよう」と考えるのではなく「このボタン要素をクリックしよう」と思って使うはずです。したがって、テストをソフトウェアが使われる姿に似せるという観点において、 user-event を使うことが推奨されています。ただし、 user-event が再現できていないブラウザの挙動もいくつか存在します。そのような挙動に対してテストを書きたいときには fireEvent を使うといいでしょう *2 。 要素の状態を検査する jest-dom 要素の状態を検査する機能として、 jest-dom というライブラリが提供されています。 testing-library.com jest のマッチャーを追加して、確認(AAA パターン *3 における Assert)をしやすくしてくれます。 提供している関数の一覧 を見るとよくわかりますが、直感的に状態を確認できるようになっています。 たとえば、要素がフォーカスされていることをテストする場合は次のように実装します。要素がどういう状態のときにフォーカスされているかという実装の詳細には立ち入らず、要素がフォーカスされていることをユーザが認識するのと同じように、テストが実装されていることがわかります。 const inputElement = screen.getByRole ( "textbox" ); expect ( inputElement ) .toHaveFocus (); 実際にテストを書いてみる 具体的なテストの例を見てみましょう。次のような UI コンポーネント に対してテストを書いてみます。 この コンポーネント は次のような仕様を満たします。 ラベル『ユーザ名』『ニックネーム』『生年月日』の入力要素がある 入力して『送信』ボタンをクリックすると、 props で渡す submit 関数が呼ばれる。入力値はその引数として与えられる 『ユーザ名』『生年月日』は必須項目である。空文字列のまま『送信』ボタンをクリックすると submit が呼ばれず、アラートメッセージが表示される 次のような二つのテストを実装します。 すべての入力要素にデータを入力し『送信』ボタンをクリックすると、データが送信されること 『ユーザ名』だけ空文字列にして『送信』ボタンをクリックすると、データが送信されずにアラートメッセージが表示されること まずは、すべての入力要素に値を入力し、 submit 関数が呼び出されていることを確認するテストを実装してみます。 test ( "入力したデータが送信される" , async () => { // 準備フェーズ const mockSubmit = jest.fn (); render (< UserForm submit = { mockSubmit } / >); const user = userEvent.setup (); // 実行フェーズ await user. type( screen.getByRole ( "textbox" , { name: "ユーザ名" } ), "太郎" ); await user. type( screen.getByRole ( "textbox" , { name: "ニックネーム" } ), "たろちゃん" ); await user. type( screen.getByRole ( "textbox" , { name: "生年月日" } ), "2023-03-27" ); await user.click ( screen.getByRole ( "button" , { name: "送信" } )); // 確認フェーズ expect ( mockSubmit ) .toBeCalledWith ( { name: "太郎" , nickname: "たろちゃん" , "birthday" : "2023-03-16" } ); } ); 実行フェーズの await user.type(screen.getByRole("textbox", { name: "ユーザ名" }), "太郎"); は、「アクセシブルな名前が『ユーザ名』であるような入力要素に "太郎" と入力する」という意味です。 確認フェーズでは、 submit が呼ばれていることと、その引数として渡されるオブジェクトを検査しています。 次に、『ユーザ名』の入力要素に値を入力せずに送信ボタンをクリックした場合のテストを実装します。このとき、 submit 関数が呼び出されず、アラートメッセージが表示されることを確認したいです。 test ( "ユーザ名の入力がないと、データが送信されない" , async () => { // 準備フェーズ const mockSubmit = jest.fn (); render (< UserForm submit = { mockSubmit } / >); const user = userEvent.setup (); // 実行フェーズ await user. type( screen.getByRole ( "textbox" , { name: "ニックネーム" } ), "たろちゃん" ); await user. type( screen.getByRole ( "textbox" , { name: "生年月日" } ), "2023-03-27" ); await user.click ( screen.getByRole ( "button" , { name: "送信" } )); // 確認フェーズ expect ( mockSubmit ) .not.toBeCalled (); const alertTextBox = await screen.queryByRole ( "alert" ); expect ( alertTextBox ) .toHaveTextContent ( "ユーザ名が入力されていません。" ); } ); 前のテストと比べて、実行フェーズにおけるはアクセシブルな名前が『ユーザ名』である入力要素への入力がなくなりました。 確認フェーズでは、 submit が呼ばれなくなったことを確認しています。また、アラートが表示され、そのテキストについても検査しています。 いずれのテストも、ユーザがアプリケーションを使う際の使用方法や状態を観測する方法がそのままテストに反映されているのがわかると思います *4 。 このようなテストは、要素の順番が入れ替わったり一部の属性が変わったりしても影響を受けにくく、人にとって理解もしやすいです。なるべくシンプルなテストを実装できるように、ぜひ testing-library の提供している API を眺めて使い方を考えてみてください。 テストが書けないケース もし、テストを書いた UI が次のような マークアップ で実装されているとどうでしょうか。 < p > ユーザ名 </ p > < input id = "name" /> < p > ニックネーム </ p > < input id = "nickname" /> < p > 生年月日 </ p > < input id = "birthday" /> < div > 送信 </ div > この状態だと先ほどのテストを実行することはできなくなります。たとえば、『ユーザ名』という文字列は id="name" である入力要素のアクセシブルな名前として認識されませんし、『送信』と書かれている要素はボタンとして認識されません。 この実装は、テスト容易性が低いという以前にマシンリーダビリティが低い状態にあります。マシンリーダビリティとは、機械にとってのコンテンツの読み取りやすさの度合いです。フロントエンドのテストは、機械がコンテンツを読み取ったり操作をしたりしてソフトウェアの動作をシミュレーションするという性質上、マシンリーダビリティが高いほうがテスト容易性が高くなる傾向にある。 フロントエンドのテストの導入の前に、マシンリーダビリティ(ひいては アクセシビリティ )の向上を目指すとよいと思います。先ほどの実装は、たとえば、次のように修正することでマシンリーダビリティもテスト容易性も高まります。 < label for = "name" > ユーザ名 </ label > < input id = "name" /> < label for = "nickname" > ニックネーム </ label > < input id = "nickname" /> < label for = "birthday" > 生年月日 </ label > < input id = "birthday" /> < button > 送信 </ button > 運用するときの注意点 テストはソフトウェアの品質の向上になくてはならないですが、それ自体がソフトウェアの品質を向上させる魔法ではありません。 ソフトウェアの品質を上げられるようなテストを実装するために、次のようなことを事前に決めておくとよいです。 単体テスト を作る目的を明確にする バリデーションを含む入力フォームの コンポーネント 、特定の API を叩く コンポーネント 、他のページへの導線がある コンポーネント などなど、多種多様な コンポーネント ごとに必要になるテストは異なります。 単体テスト を作る目的は、ソフトウェアの退行に対する保護や リファクタリング への耐性を与えて、ソフトウェア開発プロジェクトの成長を持続可能にすることです *5 。しかし、何をもってして「持続可能である」と主張するかは人によって変わります。 どれだけのシナリオをカバーするテストを実装するか ソフトウェアがあるシナリオで仕様通りに動作することを検査するにはどのようなテストがよいか どういう状態のとき、"よいテストである"といえるか これらは、チームの思想や対象となるソフトウェアによって回答は様々でしょう。 カバレッジ を上げることを目的としてテストが大量に実装しても、これらを軽視してしまうと技術的負債になってもったいなんです。テストに取り組む前に、テストを実装する目的をしっかり考えられるとよいテストの実装ができると思います。もちろん、これらのことはフロントエンド以外のテストでも同じです。 アクセシビリティ の ガイドライン を定める 前述のとおり、マシンリーダビリティが高いほど testing-library を使ったテストが実装しやすくなります。 testing-library を使ったテストの品質を高めるためには、 アクセシビリティ の向上が必須と言ってよいでしょう *6 。 ただし、テストの品質の向上のためだけに アクセシビリティ の向上を目指すと、本当にユーザによってアクセシブルなソフトウェアになるとは限りません。 アクセシビリティ を向上させる目的がわからなくなってしまっては本末転倒です。 たとえば、 img タグに alt 属性を付与すると、 getByAltText を使って要素を取得することができます。一方、 aria-label を付与すると、 getByRole を使ったクエリで要素を取得することができるようになります。テストだけを考えると getByRole のほうがクエリの優先度が高いので aria-label 属性を付与するほうがよいように感じます。しかし、 alt と aria-label では挙動がことなり、一概に aria-label を使うことがよいとは言えません *7 。テストはあくまで内部品質の向上のために実装されるもので、内部品質の向上ために外部品質を棄損するのは避けたほうがよいです。 alt 属性を使った実装のほうがユーザ体験がよくなると判断したなら、 testing-library のクエリの優先度は無視して実装をするべきです。 まずはテストを気にせずに、 アクセシビリティ の ガイドライン を定めるのがよいでしょう。そして、制定された ガイドライン をもとにした実装に対するテストの実装方法について検討します。 もし、 ガイドライン に沿った実装ではテストを実装しづらいと感じるならば、ユーザへの影響がない範囲で ガイドライン を改定するのがよいと思います。 おわりに テストはユーザ体験に影響を与えませんが、開発者体験の向上に大きく寄与します。せっかくテストを作るのだから、より効果的なテストを書いて開発者体験を向上させたいです。 今回はフロントエンドの 単体テスト という観点でよいテストの書き方について考えましたが、 VRT や E2E テストでは、また違った観点が必要になります。様々なテストを使いこなすまでの道のりは長いですが、少しづつ改善していけるように努力していきたいです。 *1 : 画像引用: https://testingjavascript.com *2 : https://ph-fritsche.github.io/blog/post/why-userevent *3 : 単体テスト の考え方/使い方 p57-58 *4 : コミュニケーションベーステストはその性質上、ユーザが知覚するままのテストにはならないです。submit が呼ばれることの検査はコミュニケーションベーステストです。 *5 : 単体テスト の考え方/使い方 p6-10 *6 : https://logmi.jp/tech/articles/328087#s3 *7 : https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html#images-that-convey-information
アバター
こんにちは、タイミーのデータ統括部でデータサイエンティストをしている小関です。 タイミーのデータサイエンスチームでは、データ分析、機械学習モデル構築に加えて、Google Cloudを主軸としたMLOps基盤の構築などの業務に日々取り組んでいます。 その中でもGoogle Cloudを主軸としたMLOps基盤の構築に関連して、 Google Cloud Professional Machine Learning Engineer認定資格 を社内制度も活用しながら取得したので、実際にした勉強の内容などを紹介したいと思います。 これから受験される方の参考になれば大変嬉しいです! 受験の動機 筆者の勉強開始時の状況 勉強方法 1. 機械学習をビジネス活用する際のベストプラクティス 1.1. Googleが考える機械学習プロジェクトのベスプラ*1を理解 2. Google CloudのML関連サービス 2.1. Google CloudのサービスをWhizlabsのコース*2に付属している練習問題を解きながら整理 2.2. Googleが考える設計パターンをアーキテクチャセンター*3から理解 2.3. TensorFlow関連サービス*4の概要を理解 3. 機械学習・統計学に関する知識 3.1. Googleが提供している無料の機械学習コース*5を一通り閲欄 合格後貰えるCertificateとノベルティ Certificate ノベルティ We’re Hiring! 受験の動機 Google CloudのML関連サービス・ML周りの思想を資格勉強を通して整理したかったため MLOps基盤におけるアーキテクチャ設計を行うためのインプットをしたかったため 弊社で始まった 資格取得支援制度 をすぐ活用したかったため 当時、ドル円レートが¥140を超えており、受験料$200を会社で負担頂けるのは大変ありがたい... 筆者の勉強開始時の状況 勉強開始時の状況によって、勉強時間や内容が異なると思うので、 本試験に必要だと思われる知識・能力における、筆者の勉強開始時の状況を3つの観点で共有しようと思います。 下記の通り、筆者の勉強開始時の状況は、機械学習の前提知識、英語能力はある程度あるが、Google Cloudに関する知識はまだ浅めといった状況でした。 Google Cloud 触り始めて4ヶ月ぐらい Vertex AI上でのMLパイプライン構築をする過程で、その他の関連サービスも一通り経験している NLP、動画像系のサービスは一度も触った事がない TensorFlowとその関連サービスの深い知識はない 機械学習 新卒から3年間データサイエンティストとして働いており、主要なML関連の理論とその実装は身についている 英語 今回の試験で要求されるReadingレベルには、余裕を持って達している 勉強方法 試験ガイド に記載されている内容と受験した所感から、 出題される問題は以下の3観点で分類出来ると感じました。 機械学習をビジネス活用する際のベストプラクティス Google CloudのML関連サービス 機械学習に関する知識 ここからは、上で定義した3観点ごとに勉強した内容を紹介させて頂きます。 前章で触れた通りGoogle Cloudのサービス周りの知識がまだ浅かった事もあり、その点を重点的に学習しました。 1. 機械学習をビジネス活用する際のベストプラクティス 1.1. Googleが考える機械学習プロジェクトのベスプラ *1 を理解 DS、MLエンジニアとして働いている方は、一度は読んでおいた方が良さそうです 出題ポイントとして、特に抑えておいた方が良い主張は以下の通りです 解くべき課題に対してMLを用いるか否かは慎重に検討する MLを用いた解決策とMLを用いない解決策を比較する際は、改善幅、コストや保守性の観点から比較する MLの活用には、十分な量かつ品質が担保されたデータがある事が重要 実際の問題では、MLを用いた解決策と非MLな解決策が選択肢にあり、問題文にある具体的な課題設定に対して適切な解決策を選択するといった形式で出題されます 2. Google CloudのML関連サービス 2.1. Google CloudのサービスをWhizlabsのコース *2 に付属している練習問題を解きながら整理 各サービスの特性やアーキテクチャの設計を理解する上で重要な観点(出題ポイント) ノーコードで実現したいのか否か ノーコードと問題に書いてある場合には、GUI系 or AutoML系のサービスを選択する モデリングの工数を掛けるのか否か モデリングの必要が無く、事前学習済みのモデルで事足りる場合は、事前学習済みのモデルを使用できるサービスを選択する 少ない工数でカスタムモデルを構築する必要がある場合は、AutoML系サービスを選択する 自由度が高くある程度工数の掛かるモデルを構築する必要がある場合には、Vertex AI Workbenchなどの開発環境系サービスを選択する Googleのベスプラに則ったアーキテクチャで構成できているか 各サービスの特性や、後述するアーキテクチャセンターからGoogleが推奨するアーキテクチャを理解する 下記のGoogle Cloud上のML関連サービスとその機能を抑えておくと良さそうです データベース系 BigQuery フルマネージド、サーバーレスデータウェアハウス BigTable 大量のデータをリアルタイムで処理することに優れているNoSQLデータベース CloudSQL My SQL, PostgreSQL等をクラウド上で動かすためのマネージドサービス 分析基盤系 Dataflow リソースのオートスケーリングなどを特徴に持つETLツール Data Fusion GUI操作でETL・ELTパイプラインを構築出来るサービス Dataproc 分散処理ツールであるHadoop/Sparkの実行環境をクラウド上で提供してくれるサービス Pub/Sub リアルタイムデータやイベントデータの取り込みを行うためのメッセージングサービス 運用系 Cloud Build CI/CDの構築、実行を提供するサービス Cloud Source Repositories Google Cloud でホストされているプライベートGitリポジトリ 前処理系 Dataprep データを効率的にクレンジング処理出来るサービス Cloud Data Loss Prevention 機密性の高いデータを検出、分類、保護する機能を提供するサービス ML系 Vertex AI Google CloudのML関連のサービスが統合されたプラットフォーム AutoML 系のサービスもこちらに全て統合された Vertex AI Workbench, Vertex AI Data Labeling, Vertex AI Feature Store等の詳細な機能の概要も抑えておくと良さそうです gcloud CLI 経由でVertex AIのjobを実行するためのコードも少し出題されます BigQuery ML SQLを使用して、BigQuery上でMLモデルを構築し、実行できるサービス BQML上でサポートされているモデル は抑えておいた方が良さそうです Recommendations AI EC向けのレコメンドシステムを提供するフルマネージドサービス 最適化したい指標によって推薦の仕方を選択出来る Kubeflow Googleが公開している機械学習ワークフローをKubernetes上で実現するためのOSS Pythonでは、 Kubeflow Pipelines SDK を用いてMLパイプラインを実装する事が出来る 自然言語系 Speech to Text API 音声データをテキストに変換するAPI Text to Speech API テキストを自然な音声に変換するAPI Natural Language AI Natural Language API 事前学習済みモデルによって、感情分析、エンティティ分析、エンティティ感情分析、コンテンツ分類、構文分析などを提供するAPI 以下のサービスは、Vertex AI AutoMLへの統合に伴い公式に 非推奨 となったが、受験当時には出題されていた AutoML Natural Language API カスタムモデルによって、Natural Language APIの機能を提供するAPI Document AI API 非構造化データを対象に、データを簡単に理解、分析、利用できるようするAPI Dialogflow チャットボットサービスを作成できるサービス 動画像系 Vision AI Vision API 事前学習済みモデルによって、画像分類、オブジェクト検出などを提供するAPI Vertex AI Vision 動画像データの取り込みからモデル構築、デプロイまで一貫して行えるサービス 以下のサービスは、Vertex AI Visionへの統合に伴い公式に 非推奨 となったが、受験当時には出題されていた AutoML Vision カスタムモデルを構築して、Vision APIの機能を提供するサービス AutoML Vision Edge エッジデバイス向けに最適化されたモデルによって、Vision AIの機能を提供するサービス Video AI Video Intelligence API 事前学習済モデルによって、動画からの物体検出などを提供するAPI 以下のサービスは、Vertex AI Visionへの統合に伴い公式に 非推奨 となったが、受験当時には出題されていた AutoML Video Intelligence カスタムモデルを構築して、Video Intelligence APIの機能を提供するサービス 2.2. Googleが考える設計パターンをアーキテクチャセンター *3 から理解 ここに載っているアーキテクチャと類似するものが実際の問題でも出題されていました 出題される形式は、アーキテクチャを構成する一部サービスが空白になっており、そこに当てはまるサービスを選択するといった感じでした 2.3. TensorFlow関連サービス *4 の概要を理解 出題数は多くなかったですが、ここに載っているTensorFLow関連サービスの名前とその機能は一応抑えておくと良さそうです 3. 機械学習・統計学に関する知識 3.1. Googleが提供している無料の機械学習コース *5 を一通り閲欄 出題ポイントとして、特に理解しておいた方が良さそうな内容は以下の通りです 機械学習モデル 問題設定に対する適切な機械学習モデルの選び方 画像分類にはCNN、時系列問題にはRNNなど TensorFlowで書かれたDNNの構造を読み取る 評価指標 Precision, Recall, F1-scoreに代表される分類問題の評価指標の定義 目的に対する適切な評価指標の選び方 不均衡データに対する評価指標の選び方 前処理 カテゴリカルデータへの適切な前処理手法の選択 One-hot encoding, Label encodingなど 数値データへの適切な前処理手法の選択 Min-Max normalization, Z-score normalization, Log scalingなど 欠損データへの適切な対応 ハイパーパラメータチューニング DNNにおけるハイパーパラメータチューニング 学習率、バッチサイズ、エポック数、ドロップアウトなど 問題設定に対する適切なデータ分割手法の選び方 k-fold, group k-fold, leave-one-out, time series splitなど 過学習への対応 適切な正則化手法の選び方 Leakageが発生しうる条件の理解とその対応 合格後貰えるCertificateとノベルティ Certificate www.credential.net ノベルティ このMachine Learning Engineer Vestを着て出社すれば、社内で一目を置かれること間違いなしです。 2年前に受験した同僚は、タンブラーを貰えたらしい。羨ましい。 Machine Learning Engineer Vest We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています! product-recruit.timee.co.jp *1 : https://developers.google.com/machine-learning/problem-framing *2 : https://www.whizlabs.com/professional-machine-learning-engineer/ *3 : https://cloud.google.com/architecture/ai-ml?hl=ja *4 : https://www.tensorflow.org/resources/libraries-extensions?hl=ja *5 : https://developers.google.com/machine-learning
アバター
こんにちは、タイミーのデータ統括部でデータサイエンティストを担当している 小栗 です。 データ統括部は、組織内におけるデータ利活用を促進するため、データ分析、予測モデル構築、データ基盤構築などの業務に日々取り組んでいます。 今回、 部署内で「心理的安全性」に関する勉強会を開催しました 。 この記事では、 勉強会の内容をもとにして「心理的安全性」について解説 したいと思います。 心理的安全性とはなにか 「心理的安全性」とは 「アイデア・質問・懸念・間違いを話すことで、罰せられたり、辱められたりしないという信条のこと」 を指します。 もう少し噛み砕くと「アイデアや意見を言っても受け入れられ、評価される環境」と表現できます。 この概念を提唱したのはハーバード大学の組織行動学者であるエイミー・エドモンドソン博士です。 彼女が行なった病院の医療ミスに関する研究が、心理的安全性の概念のベースになりました [ 1 ]。 研究の結果、優秀な医療チームのスタッフは日頃から小さなミスや懸念点を率直に話し合っており、医療ミスが少ないことが分かりました。 その一方で、優秀でない医療チームでは、スタッフは日頃のミスや懸念点を報告せず、医療ミスが発生していることに気づきました。 この優秀な医療チームが持つ風土に、彼女は「心理的安全性」と名前をつけました。 その後、Googleの研究チームが「心理的安全性が高いチームはパフォーマンスが高い」という研究結果を発表すると、心理的安全性の知名度は格段に向上しました。 現在では、組織にもっとも必要な要素であると広く認知されていると思います。 心理的安全性が低い組織に存在する4つのリスク 心理的安全性について、もう少し解像度を上げて解説してみます。 エドモンドソン博士は、 心理的安全性が低い組織には対人関係における4つのリスクが存在する としています。 ◯◯だと思われたくない なので… 無知 必要なことでも質問・相談ができない 無能 自分の考えが言えない、ミスを隠す 邪魔 必要でも助けを求めず、不十分な仕事で妥協する 否定的 素直に意見を言えず、議論ができない 心理的安全性が低い組織では、これら4つのリスクの存在により、メンバーが恐怖を抱いてしまい表の右側の行動につながります。 その結果、 組織内のコミュニケーションが不足したり、アイデアが出なかったりと、組織の生産性が下がってしまう と考えられます。 心理的安全性を高めるメリット 次に、心理的安全性を高めるとどんな効果があるのか、エドモンドソン博士の著書で紹介されていた研究をもとに見ていきます [ 1 ]。 組織学習が促される メリットの一つに、組織学習が促進される点が挙げられます。 病院の集中治療室を対象に行われた研究では、心理的安全性が高いチームでは「知識の共有」などのチームベースの学習が活発に行われていました。そして、チームベースの学習は手術の成功率と関連があることが示されています [ 2 ]。 Googleの研究チームが行った研究では、Google社内においてチームの効果性にもっとも影響を与える因子は心理的安全性だと結論づけられています [ 3 ]。 そして、心理的安全性の高いチームのメンバーには以下のような特徴があることを報告しています。 離職率が低い マネージャーから評価される機会が2倍多い 他のメンバーが発案したアイデアをうまく活用できる 心理的安全性が高い組織では組織学習が促進され、組織と個人が高いパフォーマンスを発揮することができる可能性があります。 多様なメンバーのポテンシャルが発揮される 別のメリットとして、多様なメンバーのポテンシャルを引き出すことができる点が挙げられます。 多様性が尊重され、かつ心理的安全性が高い組織では、メンバーのパフォーマンスが高くなる傾向があることが研究で明らかにされています [ 4 ]。 この傾向は、組織内のマジョリティに属するメンバーより、マイノリティに属するメンバーにより強くみられました。 これは、心理的安全性はマイノリティに属するメンバーにとって特に重要なものであることを示唆しています。 別の研究では、メンバーが持つ専門知識の多様性が高いチームと、画一的なメンバーを集めたチームの比較を行なっています [ 5 ]。 研究の結果、専門知識の多様性が高いチームは、心理的安全性が高い場合は同条件の画一的なチームよりパフォーマンスが高くなる傾向があり、逆に心理的安全性が低いと画一的なチームよりパフォーマンスが低くなる傾向があることがわかりました。 多様性を尊重する組織においては、心理的安全性も併せて高めていくことで、各メンバーが高いパフォーマンスを発揮する土壌をつくることができそうです。 心理的安全性の落とし穴 一方で、心理的安全性をただ高めればいいというわけではなさそうです。 よくある落とし穴は、心理的安全性を高める努力をした結果、いわゆる「ぬるい職場」になってしまうことだと思います。 エドモンドソン博士は 「心理的安全性は、ただ親切にすることでも、パフォーマンス目標を下げることでもなく、その逆である」 と述べています [ 6 ]。 そして、 「心理的安全性」と「目標達成に対する責任感」どちらも高い状態である”Learning zone”を目指すべき としています。 心理的安全性を高めるだけに注力するのではなく、他の要素にも着目して組織の生産性を高めていくことが重要そうです。 引用: Amy C. Edmondson, “The Competitive Imperative of Learning”, Harvard Business Review 心理的安全性を高める方法 次に、心理的安全性を高める方法について考えてみます。 心理的安全性を高める4つの因子 日本国内で心理的安全性を広める活動をされている石井遼介さんは、 心理的安全性の高いチームをつくるためには、「話しやすさ」「助け合い」「挑戦」「新奇歓迎」という4つの因子が重要 だとしています [ 7 ]。 そして、それぞれの因子を満たすために必要なアクションを提唱しています。 心理的安全性を高める因子 因子を満たすアクション例 話しやすさ 意見をもらったら真っ先に「ありがとう」と伝える 助け合い 積極的に相談する、相談に乗る 挑戦 間違うことは悪くない、そこから学ぶことが重要だと伝える 新奇歓迎 違いを良い悪いではなく、ただ違いとして認める 上の表ではそれぞれの因子とアクション例をまとめていますが、勉強会ではもう少し多くのアクションを紹介しました。 気になる方は、 石井さんの著書 を読んでみてください。 タイミーのデータ統括部で実際に取り組んだこと タイミーのデータ統括部では、心理的安全性を高める第一歩として今回の勉強会を開催しました。 (とは言っても、心理的安全性について理解が深いメンバーがほとんどなので、「知識の再確認」といった感じでしたが…笑) 勉強会の後半では、心理的安全性をテーマとしたグループワークに取り組みました。 グループワークでは「心理的安全性を下げる対応をしてしまいがちな状況において、心理的安全性を高めつつ効果的に問題解決に近づくにはどういった行動をすればいいか」というテーマで議論を行いました。 例えば、以下のような状況における対応を参加者で議論する、といったものです。 とある問題に対する解決策についてチームで議論しているとき、メンバーの一人がある解決策を提案しました。 しかし、あなたのこれまでの経験や視点から考えると、その解決策は筋が悪いように思えます。 こんなとき、どう対応するのが良いでしょうか? 参加者からは、以下のような意見が挙がりました。 筋が悪かったとしても、「意見してくれたこと」に対して感謝を伝える メンバーが提案した解決策の軌道修正を一緒にやる 筋が悪いとはそもそも捉えず、メンバーがその解決策に至った思考プロセスを掘り下げて、認識や思考の違いを理解する 唯一絶対の正解がない問題をテーマに議論することで、自分一人では思いつかない対応や着眼点などが具体化され、有意義な時間になったと感じています。 勉強会やグループワークは地道ではありますが、大きな手間がかからないので取り組みやすいですし、心理的安全性について考えるキッカケになります。 組織の心理的安全性を高める手段のひとつとしておすすめです。 さいごに 近年、心理的安全性は組織にとって重要だと広く認められるようになりました。 しかし、心理的安全性の担保された組織をつくることは実際には想像以上に大変です。 タイミーのデータ統括部は心理的安全性が高い環境だと私は感じていますが、メンバーがどんどん増えていくフェーズを迎えていることもあり、心理的安全性をより一層意識し、高いレベルで担保できるよう努力したいと考えています。 We’re Hiring! タイミーのデータ統括部では、ともに働くメンバーを募集しています! product-recruit.timee.co.jp
アバター
こんにちは、タイミー開発プラットフォームチームで業務委託として働いている 宮城 です。 タイミーはリリースから4年が経過したプロダクトで、2022年の前半から一部領域でGraphQLを利用し始め現在導入を進めています。 本記事では、GraphQLをプロダクトに導入する上で判断に迷った箇所や課題に対して、タイミーでの意思決定とその理由を紹介します。参考にしていただければ幸いです。 GraphQLの選定理由についてはこの記事では触れませんが、CTOの @kameike が以下のイベントで詳しく紹介する予定です。まだ参加申し込みは可能ですので、興味がある方はぜひ合わせてご覧ください。 timeedev.connpass.com なお、本記事のタイトルはソウゾウさんの以下の記事にインスパイアされています。 engineering.mercari.com GraphQLの「Getting Startedの次にぶつかる壁」について多く言及しており、プロダクトに導入する上で非常に有用な記事でした。合わせて一読することをオススメします。 前提となる技術スタック 技術選定において採用したライブラリ graphql-ruby Apollo Client @graphql-codegen/cli graphql-batch rubocop-graphql, graphql-eslint バックエンドで考えたこと graphql-rubyのデフォルトのディレクトリ構造を変更 バックエンドのテスト方針 Application-level Interface-level Transport-level nullability ページネーション query/mutationによって接続するDBのreader/writerインスタンスを切り替える Datadogによるリクエストの監視 フロントエンドで考えたこと GraphiQL Fragment Colocation Testing, Storybook オンボーディング 終わりに 前提となる技術スタック 「タイミー」の技術スタックを紹介します。バックエンドはモノリシックなRuby on Railsで構築されており、働き手となるワーカー向けモバイルアプリ(Swift/Kotlin)・雇用主となる企業向けWebアプリケーション(Rails SSR/Next.js)・社内メンバー向けWebアプリケーション(Active Admin/Next.js)の3つのアプリケーションを提供しています。 このうちWebアプリケーションはそれぞれNext.jsとRails APIを利用した構成への移行を進めており、その領域で利用する技術としてGraphQLを導入することにしました。 技術選定において採用したライブラリ GraphQLを導入する上で、最終的に採用したライブラリは以下です。 バックエンド: graphql-ruby クライアント: Apollo Client クライアントコード生成: @graphql-codegen/cli dataloader: graphql-batch linter: rubocop-graphql, graphql-eslint 一つひとつ選定理由を紹介していきます。 graphql-ruby github.com RubyでGraphQLサーバーを構築する上でデファクトスタンダードとなっているライブラリです。ShopifyやGitHubで長期的に利用されておりメンテナンスの継続可能性は高いと判断しています。タイミーではメンテナであるRobert Mosolgoさん *1 のスポンサーもしています。 GraphQLの仕様やベストプラクティスに従った実装がしやすいことが特徴的で、例えばページネーションのベストプラクティスであるCursor Connections *2 を追加実装なしで利用可能です。その他にも、ドキュメントの手厚さやテストの書きやすさなどから非常に使いやすいライブラリだと思います。 Apollo Client github.com フロントエンドのGraphQLクライアントにはApollo Clientを選定しました。ReactにおけるGraphQLクライアントの他の選択肢はRelay *3 , urql *4 , graphql-request *5 などがありますが、以下の観点からApolloを選択しました。 コミュニティが活発であり、利用者・インターネットの情報・関連ライブラリの種類等それぞれ大きいため、これから数年は利用が可能と想定できる graphql-rubyでフルサポートされているためApollo/graphql-rubyそれぞれで特に設定なしに利用可能であり、統合時に詰まるポイントが少なそう とはいえ、Apolloを選択する上で気になるポイントはいくつかあります。 正規化されたキャッシュがプロダクトのユースケースに即しているのか React Suspenseに対応していない *6 代替として比較したのがurqlで、Suspenseが使える・シンプルなドキュメントキャッシュ・軽量なバンドルサイズなど利点はかなり多いものの、以下の理由から選択せずにいます。 コミュニティの小ささと利用者の少なさの点でApolloに劣り、新しくGraphQLのキャッチアップを始めるタイミーにおいてはマッチしないと考えた。運営母体が小さいのも気になる。 Apollo, Relayは思想が違うためそれぞれ残り続けるだろうが、urqlが残り続けるかどうかについては不確実性がまだ高いと判断した あなたのプロダクトにApollo Clientは必要ないかもしれない *7 という一休さんの記事でApolloの向き不向きに関する言及があるが、(Suspense以外で)urqlで実現可能でApolloで実現不可能なことはないため、問題が出てきてから乗り換える形で問題ないと考えた 後述するgraphql-codegenはApolloとurqlの両方に対応しているため、Apollo Link *8 やキャッシュストラテジーの複雑なカスタマイズなどのようなApollo特有の機能を多用しなければ乗り換えは難しくない しかしGraphQLに精通したメンバーが多ければおそらくurqlを選択していたかなとも思います。クライアントライブラリについては運用を続けながら検討したいと考えています。 @graphql-codegen/cli www.the-guild.dev GraphQLサーバーから提供されるスキーマを基にReactのカスタムフックやTypeScriptの型を生成してくれるライブラリです。個人的にGraphQLを利用する大きな理由の1つであり、これのあるなしで開発体験が大きく変わるほどだと思っています。 各種設定などは後述のフロントエンド周りの実装方針の項で詳しく述べます。 graphql-batch github.com DataLoaderをRubyで実装するためのライブラリとしてgraphql-batchを選択しました。 GraphQLのクエリでは取得したいデータのノードを辿って必要なデータを一度に取得できるため、しばしばN+1が起きてしまいます。しかしどのフィールドの組み合わせが要求されるかはクエリによって異なるため、ActiveRecordモデルのassociationsの取得に対して単純にpreloadやeager_loadを行うのは無駄な読み込みが増えてしまい良い解決策とは言えません。 そのための解決策としてDataLoaderを利用した遅延読み込みを実装します。Rubyでの実装方法としては、今回選択したgraphql-batchかgraphql-ruby同梱のdataloaderの2つが選択肢として上がりそうです。 どちらも動かしてみた上で、今回はgraphql-batchを選択することにしました。 graphql-batch Shopifyがメンテナンスしており、ある程度枯れているといえる。ShopifyがGraphQLを利用し続ける限りはメンテナンスが続くと想定できる 大元のnode実装のdataloaderのAPIに近く、他言語での実装経験がある人は理解しやすい とはいえgraphql-rubyのfield extensionなどの複雑なことをしようとする場合、Promise.rbのキャッチアップが必要なのはデメリットか graphql-ruby同梱のdataloader 後発のためAPIが直感的で使いやすい印象 graphql-batchではPromiseオブジェクトをresolveしなければオブジェクトが手に入らない場面があったが、直接ActiveRecordオブジェクトが返ってくるため理解しやすい 2021年リリースで比較的新しい。内部的にはRubyのFiberを利用しているが、Fiberはデバッグが難しく問題が出てきた際の解決は難しい可能性が高い 今回は安全を取ってgraphql-batchを選択しましたが、何かあった際のgraphql-ruby同梱のdataloaderへの移行(またはその逆)は難しくないというのもあり暫定で意思決定しています。運用しながら判断をする予定です。 rubocop-graphql, graphql-eslint github.com github.com 新しくGraphQLを学ぶメンバーが多い環境のため、GraphQLの思想やベストプラクティスを学ぶためにも初期段階でLinterを用意しておくのは良いと判断し、上記2つを導入しました。 graphql-eslintについてはparserとしての役割も担い、VSCode上でgraphqlファイルを書く際の補完も有効になるのが便利です。 バックエンドで考えたこと ここからは実装を進める上でぶつかった課題についてそれぞれ述べていきます。まずはバックエンドから。 graphql-rubyのデフォルトのディレクトリ構造を変更 graphql-rubyではgeneratorが付属しており、 rails generate graphql:install で必要なファイル群が生成され以下のようなディレクトリ構成になります。 ❯ tree app/graphql app/graphql ├── mutations │   └── base_mutation.rb ├── timee_schema.rb # Railsアプリケーション名から自動で命名される └── types ├── base_argument.rb ├── base_connection.rb ├── base_edge.rb ├── base_enum.rb ├── base_field.rb ├── base_input_object.rb ├── base_interface.rb ├── base_object.rb ├── base_scalar.rb ├── base_union.rb ├── mutation_type.rb ├── node_type.rb └── query_type.rb このコード群のうち気になる点がいくつかありました。 TimeeSchemaや追加された全てのクラスにおいて、名前空間がグローバルに設定されている typesディレクトリのクラスは Types::BaseArgument となり、TypesモジュールはGraphQL以外でもよく使われるはずで名前空間の範囲が広すぎる たくさんのbaseクラスがtypesディレクトリにヒラ出しになっている。それぞれのbaseクラスを継承した具象クラスをこのディレクトリに追加していく場合すぐ見通しが悪くなることが見込まれる そのため以下のようにディレクトリ構成を変更しました。 ❯ tree app/graphql app/graphql └── graphql_schema ├── arguments │   └── base.rb ├── connections │   └── base.rb ├── edges │   └── base.rb ├── enums │   └── base.rb ├── fields │   └── base.rb ├── input_objects │   └── base.rb ├── interfaces │   └── base.rb ├── mutations │   └── base.rb ├── objects │   ├── user_type.rb ... 具象クラスの例 │   ├── base.rb │   ├── mutation_type.rb │   └── query_type.rb ├── scalars │   └── base.rb ├── timee_schema.rb └── unions └── base.rb graphql_schemaディレクトリにラップし、それぞれのtypeごとにディレクトリを用意しています。 GraphqlSchema::TimeeSchema などのクラス名になり、GraphQL関連のクラスは全てGraphqlSchemaネームスペース下に含まれることになります。 これにより外からこれらのクラスを参照することがもしあったとしたら何かおかしいと気づけるはずです。 private_constant を利用して機械的に可視性の制限もできます。 このディレクトリ構成で困ったことはほぼなかったのですが、 rails generate graphql:object などのgraphql-rubyが提供するScaffoldジェネレーターがそのまま利用できない問題がありました。ActiveRecordモデルに対応するオブジェクト型クラスを作る場合、モデルのattributesからフィールドの型を類推しコード生成してくれるため非常に便利で、どうにか活用したいです。 ジェネレーターのテンプレートで Types モジュール下にクラスがある想定なのが原因で *9 、ジェネレータークラスを継承したカスタムジェネレーターを作ることで解決できました。 # lib/generators/timee/graphql/object/object_generator.rb require ' generators/graphql/object_generator ' module Timee module Graphql class ObjectGenerator < :: Graphql :: Generators :: ObjectGenerator source_root File .expand_path( ' templates ' , __dir__ ) def create_type_file template ' object.erb ' , "#{ options[ :directory ] } /graphql_schema/objects/ #{ subdirectory } / #{ type_file_name } .rb " end # idフィールドはGraphQL::Types::Relay::Nodeで実装するため除外する def normalized_fields super @normalized_fields .reject! { _1.instance_variable_get( :@name ) == ' id ' } end end end end # lib/generators/timee/graphql/object/templates/object.erb <% module_namespacing_when_supported do -%> module GraphqlSchema module Objects class < %= ruby_class_name %> < Base implements GraphQL::Types::Relay::Node <% normalized_fields.each do |f| %> <% = f.to_object_field %> <% end %> end end end <% end -%> これにより rails generate timee:graphql:object MyNamespace::ModelName でオブジェクト型クラスをScaffoldingできます。 バックエンドのテスト方針 チーム開発でアーキテクチャを安全にスケールさせるためには、定義したレイヤーに対応したテスト方針を用意しておくことは開発メンバー間での認識を揃えるために有用と考えています。graphql-rubyのドキュメントには以下の3つのレイヤーとテスト方針が紹介されており *10 、それを基にタイミーでのテスト方針を定めました。 Application-level 認可やビジネスロジックのレイヤーです。 ActiveRecordモデルやPOROなどのGraphQLとは別のレイヤーなどにロジックを書き、単体テストを書くのが良いでしょう。 Interface-level GraphQLの各query/mutationのレイヤーです。 各query/mutation単位で期待するフィールドが返るかを検証するテストを書くのが良いでしょう。以下はgraphql-rubyのドキュメントに記載されている参考例です。 it " loads posts by ID " do query_string = <<- GRAPHQL query($id: ID!){ node(id: $id) { ... on Post { title id isDraft comments(first: 5) { nodes { body } } } } } GRAPHQL post = create( :post_with_comments , title : " My Cool Thoughts " ) post_id = MySchema .id_from_object(post, Types :: Post , {}) result = MySchema .execute(query_string, variables : { id : post_id }) post_result = result[ " data " ][ " node " ] # Make sure the query worked assert_equal post_id, post_result[ " id " ] assert_equal " My Cool Thoughts " , post_result[ " title " ] end MySchema.execute に対してクエリ文字列やvariablesを渡し、期待するレスポンスが返るかどうかを検証しています。 API実装時には基本的にこのテストを書くことが多くなるはずです。 Transport-level GraphQLサーバーはHTTP(Railsのrouting)で提供しています。 疎通確認のために1件だけrequest specを用意しました。個別のquery/mutationについてテストする必要はありません。 nullability graphql-rubyで生成したschema.graphqlを見ると、connection_typeのnodesフィールドなどほとんどのフィールドがnullableで定義されていることに気づきます。自身でフィールドを定義する場合もデフォルトではnullableであり、non-nullにしたければ明示的に null: false を付与する必要があります。 この仕様を知った時、なぜnullableなのか?と違和感がありました。nullableではフロントエンドの多くの箇所で存在チェックをしなければならなくなり、無駄にコードを複雑にしてしまいます。 GraphQL公式ドキュメントのベストプラクティス *11 によると「データベースやその他のサービスに支えられたネットワークサービスでは、うまくいかないことが度々あるからだ」と述べられています。またWhen To Use GraphQL Non-Null Fields *12 というブログ記事では「GraphQLはバージョンレスAPIの思想を持ち、多くのチームが一つのAPIに依存するため破壊的変更が難しくなりやすく、スキーマの進化を困難にさせる」と述べられています。 チームでの議論の上、そうした背景も踏まえつつもREST APIでのリソース設計と同様で「理由がない限りnon-nullのフィールドとする」方針で進めることにしました。理由は以下です。 non-nullのフィールドに対してDBから返却されたデータがnullであった場合、graphql-rubyがそのクエリ自体をエラーとしてハンドリングしレスポンスを組み立ててくれること そもそもそのような状態はビジネスロジックの要求を満たせていないことが多いはずで、nullableにしてしまうことで問題に気づけない状況を防ぎたい 一般的にnon-nullからnullableに変更する方が簡単であり、その逆は困難を伴うか不可能な場合が多い GitHubのAPIでは、connection typeのほとんどがnon-nullとなっている e.g.) https://docs.github.com/en/graphql/reference/objects#organization プロダクトの特性上、破壊的変更に対処することが難しいわけではない GraphQL APIを外部に公開する予定がなく、ステークホルダーの調整も社内で完結する クライアントは現状Webのみのため、オンデマンドなリリースが可能 graphql-rubyの場合、non-nullにオプトインする設定が用意されているためそれを利用すれば良いです。 https://graphql-ruby.org/api-doc/2.0.14/GraphQL/Types/Relay/ConnectionBehaviors/ClassMethods#node_nullable-instance_method ページネーション graphql-rubyでは標準のページネーションとして cursor-based pagination の仕組みが導入されています。 cursor-based paginationでは前後のページのカーソルのみ手に入るため、Googleの検索結果にあるような ページ番号を選択して直接ページに飛ぶ ことができません。 タイミーでGraphQLを導入するのは主に管理画面であり、管理画面のようなプロダクトでページ番号による操作が行えないことは困る場合が多いため、もう1つのページングアルゴリズムであるoffset-based paginantionを自前で実装しています。 主にこれらの記事を参考にしています。 Generic page number / per-page pagination with GraphQL-Ruby · GitHub ichikawa-dev.hatenablog.jp query/mutationによって接続するDBのreader/writerインスタンスを切り替える タイミーではRailsのマルチDB機能を利用しているため、GraphQLでも活用するためにリクエストに含まれているquery/mutationを識別してDBインスタンスを切り替える仕組みをgraphql-rubyのtracerを使って実装しました。詳しくはこちらに記載しています。 zenn.dev Datadogによるリクエストの監視 GraphQLは単一のPOSTエンドポイントに全てのリクエストが送信されるため、通常のAPMを利用したエンドポイントの監視では適切な監視を行えません。そのためgraphql-rubyでは、リクエストをトレースしAPMとの統合を行える仕組みが用意されています。タイミーではDataDogを利用しており、graphql-rubyがデフォルトでDataDogとの統合が提供されていました。 これにより、GraphQLのoperation name単位で分類しつつ監視できます。 フロントエンドで考えたこと GraphiQL GraphiQLは、GUIでGraphQLを操作するための統合開発環境です。コード補完・シンタックスハイライト・APIドキュメントの閲覧と、実際にquery/mutationも実行可能です。 graphql-rubyでは https://github.com/rmosolgo/graphiql-rails が初回セットアップ時に追加されるためそれを使うのが一般的かもしれませんが、graphiql-railsを使うとなるとタイミーの場合APIリクエストをする際の認可プロセスをNext.js用に用意しているものとは別で用意しなければならず、理想とはいえませんでした。 npmで公開されている https://www.npmjs.com/package/graphiql が提供しているReactコンポーネントが利用できることに気づき、そちらをNext.js上で利用することで解決しました。どんなクエリでも実行できる自由さは危険なため、Production環境では使用できないようにしています。 Fragment Colocation 今回はプロジェクト開始時点でFragment Colocationの方針で進めることにしました。Colocationは「一緒に配置する」という意味とのことで、GraphQLのFragment定義とコンポーネントを近い場所に置き、コンポーネントに必要なデータを宣言的に定義する方針を取っています。 より具体的な概念や実現方法はこのスクラップで詳細にまとめられています。 zenn.dev RelayではFragment Colocationが “most important principle” とされていて厳格な運用を強制されますが、Apolloではそこまで堅い設計になっていません。実際の運用としてはgraphql-codegenのnear-operation-file *13 プラグインを利用することで運用が可能です。graphqlファイルの同階層にコード・型を生成したファイルを配置してくれます。以下がcodegen.ymlの設定例です。 overwrite : true schema : 'src/apis/graphql/schema.graphql' documents : - "src/**/*.graphql" generates : src/ : preset : near-operation-file presetConfig : extension : .generated.ts baseTypesPath : ~~/apis/graphql/types.generated plugins : - 'typescript-operations' - 'typescript-react-apollo' config : immutableTypes : true nonOptionalTypename : true avoidOptionals : true ~~~ 省略 ~~~ Fragment Colocationを運用することで以下のメリットがあると判断しています。 クエリの実行はPageコンポーネント・各コンポーネントで必要なデータはfragmentとして定義するようにルール化すると、初回ページ表示に必要なネットワークリクエストを1回で終えられる 必要なデータがコンポーネントの横に定義されているので見通しが良い・不要な定義の削除漏れに気付きやすい・underfetching/overfetchingに気付きやすい 生成コードを1つのファイルにまとめるデフォルトの方法と比べてファイルチャンクの最適化が可能、ページに必要なコードのみを含めることができる ref: https://blog.hiroppy.me/entry/2021/08/12/092839 課題としてfragmentでvariablesを定義できないことの不都合や、mutationのrefetchQueriesとの相性が悪いなどは考えられますが、それを差し置いてもメリットは大きいと思っています。 Testing, Storybook コンポーネントのテストはGraphQLがなくても変わらず、Storybookを起点に行なう方針としています。 Fragment Colocationを利用しているため大半のコンポーネントは必要なデータに相当するpropsを渡すだけで問題ないので考えることは少ないですが、ネットワークリクエストが発生するPageコンポーネントではリクエストのモックを検討する必要があります。 モックを実現するためのライブラリとしてはstorybook-addon-apollo-client *14 とMock Service Worker *15 の2つが選択肢として考えられますが、チームの学習負荷が大きい状況だったため一旦は学習コストの少ないstorybook-addon-apollo-clientを選択しました。しかしGraphQLの導入とは別の課題感からmswを導入する動きがチームで始まっており、ゆくゆくはmswに載せ替えていきたいと考えています。 またモックデータを簡単に生成するための仕組みとしてgraphql-codegen-typescript-mock-data *16 を導入しています。Railsでよく使うFactoryBotのように簡単にオブジェクトが生成できてかなり便利です。概要はこちらの記事が理解しやすいかと思います。 zenn.dev オンボーディング 最後に組織へのGraphQLの浸透のためのオンボーディングについて紹介させてください。 タイミーでは組織の拡大に伴って多くの課題が生まれては乗り越えてきました。現在は書籍チームトポロジーを組織内の共通言語として扱う取り組みを進めつつ、チームトポロジーをベースにチームの役割の再定義・分割を進めています。その過程で事業価値を最大化を目指す複数のチーム(チームトポロジーで紹介されているストリームアラインドチーム)の他に、ストリームアラインドチームを支援する開発プラットフォームチームを立ち上げました。 *17 開発プラットフォームチームは、ストリームアラインドチームが自身の責任領域に注力しやすくするためにバリューストリームから遠い技術的な領域に関する認知負荷の削減をミッションとしています。 筆者も開発プラットフォームチームに所属しGraphQLの導入に取り組んできましたが、ストリームアラインドチームが解決したい課題を解決するためにGraphQLを利用できて、すぐにバックエンドのquery/mutationを、フロントエンドのGraphiQLでクエリをそれぞれ書き始められる状態を目的としていました。 合わせて弊チームはチームトポロジーでいうイネイブリングチームとしての側面も持ち、GraphQLのスキルをストリームアラインドチームが習得するための短期的な支援も担っています。そのための支援の例としてモブプログラミング会を開催したり、社内記事の充実化を図ったりしています。本記事は社内ドキュメントに書き連ねていた内容からプロダクト固有の内容を省略しまとめたものだったりもします。 社内記事の例 またGraphQLを利用した実装で判断に迷う時の議論を減らすための土台としてコーディングスタイルガイドの制定に取り組んでいます。まだ進行中ではありますが、目次だけ紹介します。 目次 ストリームアラインドチームが実際に機能実装をする過程で出てくる課題によって加筆修正を繰り返していく予定です。本文が読みたい方はぜひ入社を、カジュアル面談もお待ちしています。 終わりに 本記事では、RailsとNext.jsを利用しているプロダクトでGraphQLを導入する際に考えたことを紹介しました。概念等の説明は少なめに、より実践的な内容の紹介を目的としていました。組織でGraphQLを導入を始める際に議論の参考にしていただけると幸いです。 ここまで読んでいただきありがとうございました。 *1 : https://github.com/rmosolgo *2 : https://relay.dev/graphql/connections.htm *3 : https://relay.dev/ *4 : https://formidable.com/open-source/urql/ *5 : https://github.com/prisma-labs/graphql-request *6 : ApolloでのSuspenseはサポート予定ではあるようです。 https://github.com/apollographql/apollo-client/issues/9627 *7 : https://user-first.ikyu.co.jp/entry/2022/07/01/121325 *8 : https://www.apollographql.com/docs/react/api/link/introduction/ *9 : https://github.com/rmosolgo/graphql-ruby/blob/db26a55a639a47f1206f5f7a3bf70ebbb61aaed0/lib/generators/graphql/templates/object.erb *10 : https://graphql-ruby.org/testing/integration_tests *11 : https://graphql.org/learn/best-practices/#nullability *12 : https://medium.com/@calebmer/when-to-use-graphql-non-null-fields-4059337f6fc8 *13 : https://www.the-guild.dev/graphql/codegen/docs/presets/near-operation-file *14 : https://storybook.js.org/addons/storybook-addon-apollo-client *15 : https://mswjs.io/ *16 : https://github.com/ardeois/graphql-codegen-typescript-mock-data *17 : タイミーのプロダクト組織の変遷やなぜチームトポロジーか、に興味がある方はこちらの記事もご覧ください。 チームトポロジー Vol. 2 「組織をチームトポロジーで振り返るメリット」タイミー 亀田 彗 | Forkwell Press | フォークウェルプレス
アバター
こんにちは、タイミーでバックエンドエンジニアをしている難波 @kyo_nanba と申します。 今回は9月8, 9, 10日に開催され、タイミーもプラチナスポンサーとして協賛したRubyKaigi 2022の参加報告になります。 こういった大規模カンファレンスは昨今の情勢もありオフラインでの開催がなかなか難しい状況でしたが、今年は 三重県 津市で現地開催されるということになりぜひ参加したいという有志が集まって参加させて頂くことになりました。 なおタイミーでは自分達がお世話になっている技術や OSS に対してコントリビュートやスポンサーなど様々な面から貢献することを推奨しており、今回のRubyKaigi参加もその一環として社内参加者には移動費や宿泊費などが補助されています。感謝 🙏 また余談ですがRubyKaigiとほぼ同時期に開催された iOSDC Japan 2022 についてもスポンサーをしております。タイミーの使用している技術などに興味を持って頂いた方はぜひ こちら をご覧ください。 当日のタイミーチーム 今回タイミーとして参加させて頂いたのはエンジニアが2名(プラス現在お手伝い頂いている業務委託の方が1名)、技術に興味があり参加したいと手を挙げてくれたカスタマーサポートの方が1名、プロダクトHRの方が2名という少し変則的なチームでした。 エンジニアの参加はもちろんなのですが、タイミーでは開発組織をよりスケールしていくために開発組織の人事企画・採用を担当する「プロダクトHR」という部署があり、その活動をより加速させていくためにもまずは知ることから始めようと、様々な知見を得る機会として一緒に参加頂きました。カスタマーサポートの方も含めエンジニア以外の職種でもプロダクトを支える技術に興味を持っている方が多くいるのがタイミーの良いところの1つかなと思います。 なお、そのプロダクトHRからも別途 RubyKaigi 2022参加記事 が公開されているので、よろしければそちらもご覧ください。 発表について 3日間様々な発表に参加させて頂きましたが、どれもとても濃い内容の発表ばかりでした。 発表で得た気づきをお互いに共有したり改めて考えてみるために後日社内の参加した人たち、参加できなかった人たちで集まって振り返り会を行ったので、ここでは振り返り会の中で話題に挙がった、特に印象に残った発表についていくつか簡単に紹介したいと思います。 Ruby meets WebAssembly Ruby meets WebAssembly - Speaker Deck まずは一日目のキーノートである " Ruby meets WebAssembly" になります。社内振り返り会でも「 Ruby で書かれたものがJSに コンパイル されWebで表示されて、今のフロントのトレンドが変わっていくのかなぁなど個人的にWebAssemblyに興味を持った」や「初学者のプログラミング学習において最初の壁となるのはやはり環境構築であり、ブラウザで動かせることは裾野を広げるためにも非常に大事」など、この辺りの技術がより深まっていくことに対する期待の声が多く挙がりました。 TRICK 2022 (Returns) GitHub - tric/trick2022 こちらについてはやはり見た目の インパク トが強く「ターミナルで実行される Ruby のプログラムの文字列自体がアニメーションされるのはすごすぎた。最初の単純な作品を見た時はこういうのを作りながら Ruby を学ぼうかなと思ったが、上位の作品が発表される度に『すごいなぁ』と思うだけになっていました。」など社内振り返り会でも驚きの声が挙がっていました。 ruby /debug - The best investment for your productivity こちらは Ruby 3.1から同梱されるようになった debug.gem についての解説でした。今まで弊社でよく使われていたのはbyebugとpryだったのですが、こちらの発表を機にモチベーションが高まり現在では社内で最も大きい Rails プロジェクトの リポジトリ もdebug.gemと irb に代わっております。 グルメについて RubyKaigiといえば開催地の名産など様々なものを楽しむのも醍醐味です。 三重県 といえば鰻や松坂牛は言わずもがな、地元のお酒や海産物など様々なグルメがあります。RubyKaigi開催中は生憎の天候になることもままありましたが、夜は比較的天候が落ち着いたこともあり、いろいろなお店で地元のグルメを楽しむことができました。 写真を見ていると三重の食事が恋しくなってしまうのでグルメの話はこの辺りにしておきましょう。 参加してのひとこと 最後に少し私事ですが、私が前回RubyKaigiに参加したのは2018年の仙台開催だったので実に4年ぶりのRubyKaigi参加でした。あの頃とは情勢の変化もあり盛り上がり方に多少の変化はありましたが、やっぱりRubyKaigiは年に一度 Rubyist が集まるお祭りといった雰囲気で、参加者のみなさんから感じるモチベーションの高さや楽しんでいる様子から自分自身ももっとアウトプットしていこう!英語を勉強しよう!という気持ちにさせられます。 次回は長野県 松本市 での開催ですが、弊社としてもブースを出したり他のスポンサードを考えたりと、もっと積極的に関わっていきたいですね。 最後にこの難しい状況の中で入念に準備を行い無事に会を成功させた主催者の方々に心より感謝を申し上げます。
アバター