Timee Product Team Blog

タイミー開発者ブログ

#DroidKaigi に向けて数字で振り返るタイミーのAndroid開発

こんにちは、タイミーDevRelの河又です。

タイミーはDroidKaigi 2024にゴールドスポンサーとして協賛しています。
当日はブースも出展しておりますので是非、お立ち寄りください。

今回はDroidKaigiを前に一度、タイミーのAndroid開発を数字で振り返ろうという企画です。 Androidエンジニアの中川をインタビューアーとしてAndroid領域のリードエンジニアである村田にタイミーのAndroid開発についてインタビューする形式でお届けします!

タイミーのAndroidアプリのクラッシュフリーレートについて

2022年


2024年現在


※グラフ上の7日間、30日間の数値は当該期間全体の数値ではなく、デイリーの移動平均の数値です

続きを読む

タイミーでOSTを開催しました

こんにちは! Agile Practice Teamでプロセス改善やアジャイルコーチとしてチームの支援を担当しています、吉野です。 2024年4月にタイミーに入社後、初めてオフラインにて社内のOST(オープンスペーステクノロジー)イベントを体験してきましたので、レポートします。

今回お話しする内容

  • どんなイベントを行ったのか?
  • タイミーでのOSTはどんな雰囲気で開催されたのか?

を、お話ししていきます!

どんなイベントを行ったのか?

今回開催されたイベントの概要

という形式でのOSTが開催されました。

参加者について

タイミーのプロダクト開発組織に関わっている方の中から参加希望者を募っての開催でした。先陣を切って動かれていたrazさん、 りっきーさんの一声で、20人近くの人が一気に集まりました!(しかも3日ぐらいで!すごい!!)

結構OSTとか慣れているのかな?と思いながら当日参加したところ、なんと今までOSTを経験したことのある参加者は1/3ほどでした。

OSTの様子

私はまだ社内のオフサイトイベントに参加していなかったので、応募時点ではみんなのモチベーションがどれぐらい高いのかわからずの参加でした。

未体験のイベントにオフサイトで参加することには、一定のハードルがあると思っていましたが、一気に参加者が集まり「みんなのイベント参加へのオープンさ」を感じることができました! イベント参加や勉強へ前向きな環境は、その場にいてめちゃくちゃテンションが上がります!

タイミーでのOSTはどんな雰囲気で開催されたのか?

どんなOSTだったのか?

枠としては、15分区切りの4枠にて開催されました。

お題がめっちゃ出てきた!

OSTの原則として、

💡 OSTの原則の一部

  • いつ始まろうと、始まったときが適切なときである
  • いつ終わろうと、終わったときが終わりのときである

があるので、1枠の長さはそこまで気にしなくても良さそうと思いつつ、15分という時間は自分が経験してきた中で一番短い時間だったので、どうなるのかな?忙しくならないかな?と少し心配でした。

ですが、そのような心配は杞憂に終わり、15分の中でみんなポイントを絞って議論したり、時間が足りなかったら自分たちでテーブルや場を用意して議論を継続したりしていました。

全体の時間としても1.5h(マーケットプレイスを含めず)という短時間での開催でした。 そんな中、各々が自主的に話したいことを話して時間を最大限に活用したOSTらしいOSTだったと思います。 (運営されていたお二人も、ひたすら運営に集中、というわけではなく話し合いにも参加して自身でも楽しむスタイルで立ち回られていました!)

どんな雰囲気だったのか?

タイミーでは、プロダクト開発に関わる多くの方がフルリモートでお仕事をしています。 そのため、今回オフサイトで集まった直後は「ワイワイ!ガヤガヤ!」というわけではなく、少し緊張している空気を感じました。

しかし、オフサイトイベントへ自主的に参加されていることもあり、いざOSTが始まると自己紹介や自身が向き合っているお仕事の紹介など、積極的にコミニュケーションを取りにいっていました。

最終的には、お互いの悩みに共感する声や、笑い声が多く飛び交い、一体感を感じられるイベントになっていたと思います。 「初めまして」とか、「社内イベントへの参加が初めてなんです」という声も多かった中、2hもしないうちに熱量の高い場になっており、人見知りな私も最後はすごく楽しませて頂きました!笑

今後のイベントにも期待をしていきたいプロダクト開発組織

私は社外のオフサイトで開催されるコミュニティイベントへの参加が好きで、よく参加しています。

社内でも、何かイベントが開催できないかな?もしくは開かれたら参加できないかな?と考えていたところ、今回のOSTイベントへ参加しました。 初めての参加でしたが、熱い議論をしたり、同じ組織の人との繋がりができたりと、とても有意義な時間を過ごすことができました。

普段からリモートでお仕事しているからこそ、オフラインで集まれた時はコミュニケーションを重視する時間に対しての熱量は高くなるのかなーと思いました!

今後も、社内のオフサイトイベントが立ち上がれば積極的に参加していきたいですし、自分でも何か開催してみたいと思います!


あわせて、主催であるりっきーさん、razさんのご感想です!

OSTにかける思い

スクラムマスターをやっているりっきーです。

弊社はフルリモートの環境のため、会議の集中力を阻害する要因(Slackの通知だったり、会議とは関係ないことを勧めたり)が多いと感じています。そこで、どのような設計であれば参加者が集中できるかを模索した結果、OSTに辿り着きました。

OSTが優れている点は「自分自身で興味あるテーマを公募する / 自分自身で興味あるテーマにサインアップする」に集約されています。誰かに呼ばれて会議に参加するのではなく、”自分自身”でアクションを起こさなければ会議がうまくいかないので、参加者としても集中して会議に参加できる環境になると考えています。

今回は最もやりやすい環境であることと、全社のイベントで出社する機会があったのでオフラインで開催しましたが、オンライン環境ではより効果が発揮できると思うので、今後は回数を増やしていければと思います。

初参加でもあり、初運営のOST、うまくいってよかった

どうもスクラムマスターやったりエンジニアやったりしてるrazです。今はエンジニアをしております。思いつきでやってみたいと言ったところ、2人や参加者の協力のもとOSTを開催できました。僕の記憶してる範囲では社内で実施するのは初めてだったと思います。

OST開催の動機

今回、全社のイベントがありオフサイトで集まる機会がありました。その会までの時間の使い方として提案させてもらいました。普段フルリモートだったり、違うチームだったりであまり会話する機会のない人も参加してくださったのはとても嬉しかったです。

なぜOSTなのか?の理由は3つあります。

  1. 実は自分が未経験で興味があった
  2. 毎週開催してるアジャイル相談会の様子から、普段話せてないことが色々ありそうだった
  3. 雑に大人数集めて開催してもなんとかなりそうだから

1と3はあまり説明することもないので省略しますが、2のアジャイル相談会について補足します。

アジャイル相談会とは、毎週水曜の夕方ごろにりっきーさんが主催してくれてるその名の通りアジャイルとかスクラムに関して気軽に相談していい会です。ただ、実際にはアジャイルとかスクラムの枠にとらわれず、組織論やマネジメントといった様々な内容で会話しています。その中でも、組織全体に関わることは、話す機会が少ないんだろうと感じていました。そこで、熱量のある人たちが集まって会話できるOSTをやれたらいいのではと思って開催しました。

開催してみた感想

一応運営ではあるので、全体の様子をみながらではありますが、極力会話に参加してテーマを盛り上げていきました。経験者が1/3程度いましたが、半数以上は未経験で何もわからない状態での参加でした。しかし、みんなの自主性の高さがあってか、初回セッションから盛り上がっていて安心しました。自分もその様子に安心できたので、2セッション目以降はより会話に集中できたと思います。途中会話に夢中になりすぎてタイムキープを忘れていたほどです笑。

みなさん初めての開催にもかかわらず適応能力が高いので、事前に出したテーマじゃないことに転換していて、会話を楽しんでいると感じました。個人的にはもう少し「どうしよ?」感に包まれるんかな?と心配してたのですが、杞憂でした笑

次回開催なるか?

今回、準備期間も会自体も短い中での開催でしたが、思ったよりも盛り上がってましたし、個人的には成功したんじゃないかと思ってます。いい成功体験になったと思うので、次回開催に向けて思っていること3つあげて、僕の感想を終わりにしようと思います。

  • フルリモートへの適応
  • 多種多様な役職や職種の人の参加
  • 組織を変革していくアクションを生み出せる会へ

フルリモートへの適応: タイミーのプロダクト開発組織は、フルリモートなのでリモートでも開催してみてもいいかなとは思いました。ただ、実際に運営してみてオフサイト環境だからこそ成り立っているものがありそうとも感じました。四半期に1度くらいのペースで、オンラインとオフラインを交互で開催できても面白そうです。

多種多様な役職や職種の人の参加: 急な呼びかけなのもあって参加できたメンバーは限られていました。組織に存在している様々な役職や職種の人が充足できてない状態でしたので、次回はより多種多様な人に参加してもらえるような会を開催できたらなと思います。

組織を変革していくアクションを生み出せる会へ: 会話メインで会は終わりました。しかし、もう少し時間を長くとれれば、会話も深まりますし、具体的なアクションを生み出したりして、組織やチームに変化をもたらす会にもしていけると思うので、そういう会を目指せればと思います!


以上レポートでした!

ハピネスドアもハッピー感想めっちゃ多かったです!

効果検証の事前設計と結果の管理について

こんにちは、タイミーのデータアナリティクス部でデータアナリストをしている夏目です。普段は主にタイミーのプロダクトに関する分析業務に従事しています。

本日はタイミーにおいて、効果検証設計を施策前に正しく行える仕組みづくりと効果検証設計・結果を一元的に管理できるデータベースについてご紹介します。

解決したかった課題

タイミーでは、プロダクト、マーケティング、営業組織などで様々な施策が行われています。しかしながら、それらの施策の結果を判断する効果検証には課題も多く存在しています。今回は以下の2つの課題にフォーカスしてブログを書きます。

  1. 効果検証設計が事前になされていない施策があった
  2. 効果検証設計や検証結果がバラバラに保管され、会社として知見が溜まっていなかった

まず1つ目の「効果検証設計が事前になされていない施策があった」に関してです。タイミーではアナリストの数も限られており、事前に全ての施策に目を通すことは難しいです。施策によっては事前に効果検証設計がされておらず、必要なログが取れていなかったり、検証に必要なサンプルサイズが担保されていなかったりと、正確な効果検証ができないケースが存在しました。

次に2つ目の「効果検証設計や検証結果がバラバラに保管され、全体として知見が溜まっていなかった」に関してです。タイミーでは様々なチームが施策を行っています。基本的に効果検証の結果はチームごとに管理されており、別のチームの人がその結果を探すことが難しいケースもありました。

取ったアプローチ

以上の2つの課題を解決するために、行ったことは主に以下の3つです。それぞれをこの項では説明していきます。

  1. 各チームが効果検証の設計と結果を記入できるNotion上のデータベースを作成
  2. 効果検証設計と結果を記入するテンプレートを作成
  3. 他アナリストや他チームへの説明の実施
1. 各チームが効果検証設計・結果を記入できるNotion上のデータベースを作成

1つ目のデータベースの作成に関しては、正確にはあるチームがすでに使用しているデータベースを少し改変し別チームにも展開しました。

イメージとしては、ダミーですが以下の画像で、行の一つ一つが効果検証設計と結果をまとめるドキュメントとなっています。チーム横断で1つのデータベースにまとめることにより、別チームの検証結果や検証方法を簡単に参照できるようになっています。

ダミーデータベース

2. 効果検証設計・結果を記入するテンプレートを作成

次は2つ目のテンプレートに関してです。データベースから効果検証ドキュメントを作る際に利用するテンプレートを作成しました。

テンプレートには、大きく効果検証設計と検証結果を書くパートの2つを用意しています。以下の画像は効果検証設計パートのテンプレートの一部です。

比較の手法には、A/Bテスト、DID、目標値との比較といった手法が入ることを想定しています。最後のScenarioは、設定したMetricsの動きによって施策担当チームの次のアクションがどう変わるのかを記入します。

このScenarioを事前に書くことによって、どのようなMetricを見るべきかが明らかになり、またそれらのMetricを計測するための手段が逆算されるはずです。  

テンプレートの一部

3. 他アナリストや他チームへの説明の実施

最後に「他アナリストや他チームへの説明の実施」に関してです。作ったデータベースやテンプレートを展開するため、他のアナリストや、マーケティング担当の部署などに資料を作って説明を行いました。概ね好評で受け入れられるまでのハードルは少なかったです。

結果

データベースを作って3ヶ月ほど経ちました。現状約10個ほどの施策チームがこのデータベース・テンプレートを利用して効果検証の設計を行っています。またアナリストからも、効果検証の設計をPdMとやりやすくなったといった声をもらっています。

残課題

残課題は2つほど明確なものがあると思っています。

1つ目は、効果検証設計のテンプレートの不十分さです。現状は受け入れやすさを重視し、意図的に効果検証のテンプレートをシンプルにしています。しかしながら、A/Bテストなどでは他にも設定をしないといけない項目はまだまだあるはずです。

2つ目は、検証結果を横断したメタ分析ができる体制になっていないことです。検証結果をチーム横断でまとめているので、過去どういった施策が当たりやすかったのかといったメタ的な分析もやりやすくなるはずです。しかしながら現状こういった分析に耐えうる設計はデータベースに表現されていません。

最後に

今回は、タイミーにおける効果検証設計に関して記載しました。弊社では分析自体だけではなく、今回のような分析をより活用するための仕組みづくりも沢山行っております。

We’re Hiring!

タイミーでは、一緒に働くメンバーを募集しています。

https://hrmos.co/pages/timee/jobs

カジュアル面談も行っていますので、少しでも興味がありましたら、気軽にご連絡ください。

Reference

A/Bテスト実践ガイド 真のデータドリブンへ至る信用できる実験とは

後編:YARD から rbs-inline に移行しました

タイミーでバックエンドのテックリードをしている新谷(@euglena1215)です。

この記事は先日公開した「前編:YARD から rbs-inline に移行しました」の後編となっています。前編では rbs-inline の紹介、移行の目的などをまとめています。前編を読んでいない方はぜひ読んでみてください。

tech.timee.co.jp

後編では実際の移行の流れや詰まったポイント、今後の展望について紹介します。

移行の流れ

YARD が日常的に書かれている状況から YARD がほとんど rbs-inline になり、YARD 関連のツールが削除されるまでの流れを紹介します。

1. 型をやっていくことを表明する

まずはバックエンド開発者に対してやっていく気持ちを表明しました。

YARD から rbs-inline への移行は自分1人で進めるよりは誰かに手伝ってもらったほうが自分ごとに感じられる方が増えると思い、表明と同時に手伝ってもらえる方を募集しました。

ここで、 @Juju_62q @dak2 の2名に立候補いただきました。ありがたい。

こんな感じで表明しました

2. rbs-inline のセットアップを行う

移行を段階的に進めるためにも、まずは rbs-inlne コメントを書いたらきちんと反映されるような状況を作ります。

タイミーでの型生成は pocke さんが作った便利 Rake Task rbs:setup をアレンジして使っています。そのため、 rbs:setup を実行することで rbs-inline による型定義も生成されるように変更しました。

また、タイミーでは sord gem という YARD コメントから RBS を生成するツールも使っています。rbs-inline はアノテーションがないメソッドにも RBS を生成するため、ただ生成しただけでは RBS が重複してしまいエラーになってしまいます。且つ、rbs-inline コメントが少なく YARD コメントが多い現段階においては YARD コメントによる RBS は捨てずに互換性を維持する必要がありました。

これらは sord gem の --exclude-untyped オプションを設定しつつ、 rbs subtract コマンドによって YARD → rbs-inline の優先順位で重複を削除することで解決しました。

--exclude-untyped オプションは名前の通り、YARD コメントがなく untyped にせざるを得ない RBS を生成結果から除外できます。ですが、 --exclude-untyped オプションもユースケースに完全に一致するものではなく、YARD コメントがない定数や initialize メソッドが untyped として生成されるので rbs-inline での記述が反映されない形になっていました。

定数が untyped になる問題は最終的に sord gem を削除するまでは解決しませんでしたが、YARD コメントがない initialize メソッドも生成されてしまう問題は以下のように生成されたファイルの中身を書き換えることで生成結果から強引に除外する対応を行いました。

  task sord: :environment do
    sord_path = 'sig/sord/generated.rbs'
    sh 'sord', '--exclude-untyped', '--rbs', sord_path
    Rails.root.join(sord_path).then do |f|
      content = f.read

      # sord は --exclude-untyped をつけていても initialize メソッドの型を出力するが、
      # rbs-inline を優先したいので削除する。
      content.gsub!(/def initialize:.*?(\n\s+.*?)*-> void/, '')

      f.write(content)
    end
  end

また、rbs-inline をセットアップした時点の rbs-inline 0.4.0 では ActiveAdmin 用のいくつかの実装にて rbs-inline コマンドがクラッシュする事象を観測していたので、rbs-inline の変換対象から除外していました。この辺りを soutaro さんにフィードバックしたところ、0.5.0 に入ったこの変更で修正してもらえました 🎉

社内でバグ報告できて便利

結果として、タイミーの RBS 生成ステップは以下のように変化しました。

Before

  1. rbs prototype で untyped ながらも全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成
  3. sord で YARD から型を生成
  4. rbs subtract で 1. で生成された型に対して重複した型定義を除外

After

  1. rbs-inline で全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成
  3. sord で YARD から型を生成
  4. rbs subtract で 1. で生成された型に対して重複した型定義を除外

rbs-inline が全体のRBSファイルを生成をしてくれるので、 rbs prototype によって型定義を生成するステップを削除しました。そのため、実質的にrbs prototype の上位互換として扱っています。rbs-inline に興味があってrbs prototype コマンドを使っているプロジェクトがあれば、とりあえず rbs prototype コマンドを rbs-inline コマンドに置き換えてみても良いのではないでしょうか。

 

また、このタイミングで開発ルールにも変更を加えています。

「新規で型アノテーションするときは YARD よりも rbs-inline を使うことを推奨する」ルールを追加しました。この時点ではコードベース上に rbs-inline によるアノテーションはほとんどなく、サンプルコードが少ないので義務ではなく推奨という形に留めています。(学んでみてほしくはありつつも、書き方分からないので rbs-inline ではなく YARD を書くことは許容する形)

3. YARD から rbs-inline への移行を進める

2.でrbs-inline 書き始められる状況を作れたので、ようやく YARD から rbs-inline への移行を進めていきます。

1.で手伝ってくれると立候補してもらった2名と一緒に方針を以下のように決めました。

「機械的に変換できる部分は機械的に変換していくが、自動変換を頑張りすぎない。手動で手直しした方が早い部分は手動で書き換える。コスパの良い方法を適宜選んで置き換えを進めていく」

また、変換スクリプトはメソッドに対するコメントを完全に変換可能なもののみ変換するという方針を取りました。

例えば以下のような YARD コメントが書かれたメソッドがあったとして

# @param x [String]
# @return [String]
def foo(x)
  x
end

YARD コメントの@return タグのみ変換できるスクリプトがあったとすると以下のように変換されます。

# @param x [String]
# @rbs return: String
def foo(x)
  x
end

この状態だと、YARD コメントから生成された RBS は (String) -> untyped になり、rbs-inline コメントから生成された RBS は (untyped) -> String になります。

今の rbs-inline コメントによる RBS の生成結果と YARD コメントによる RBS の生成結果だと YARD コメントが優先されるため、結果として (String) -> untyped が最終的な型になります。

元々 YARD コメントだけで記述していた際は (String) -> String と正しい型が定義できていたのに、YARD と rbs-inline が混在することで型情報が落ちてしまうことは意図したものではないため、完全に変換できるもののみ変換対象としました。

前述した方針より、YARD タグの全てをサポートしているわけではないためライブラリとしての公開は控えたいと思いますが、興味ある方向けにソースコードは公開します。興味があればご覧ください。

yard-rbs-inline-sample/tasks/yard_to_rbs_inline.rake at main · euglena1215/yard-rbs-inline-sample · GitHub

また、実行例として YARD アノテーションが多く記述されている yard gem に対して変換スクリプトを実行した結果も載せておきます。

github.com

コードを書かずにエディタの一括置換機能を使って移行したものもいくつか存在します。

  • yard-sig の記法 @!sig@rbs に置換
  • @example に対応する rbs-inline はないので NOTE: に置換
  • @see に対応する rbs-inline はないので refs に置換
  • @raise に対応する rbs-inline はないので Raises に置換
  • @deprecated に対応する rbs-inline はないので Deprecated に置換

数が少なく手動で手直しした例も挙げておきます。

  • YARD のタプル(e.g. Array(String, Integer))を使っている箇所を rbs-inline に修正
  • YARD の @option タグを rbs-inline で Hash のリテラルに修正
# @param [Hash] h
# @option h arg1 [String]
# @option h arg2 [Integer]
# @return [Integer]
def foo(h) = h.size

# @rbs h: { arg1: String, arg2: Integer }
# @rbs return: Integer
def foo(h) = h.size

これらの作業でコードベースからほとんどの YARD が rbs-inline のコメントに移行が完了しました 🎉

移行期間としてはサブタスクとして取り組んで1ヶ月半ほどでした。このタスクに集中すれば1~2週間で終わったのではないかと思います。

4. 後片付け

YARD コメントがほとんどなくなったので YARD 関連のツールを削除を始めとする後片付けを進めていきました。基本的にはスムーズに進んだのですが、進めていく中でハードルとなった点をいくつか紹介します。

sord gem の削除

YARD コメントから RBS を生成する sord gem を削除しようとしたところ、Data, Struct の型定義が見つからなくてエラーが発生しました。

sord が Data.defineStruct.new に対応する型定義を生成していたのに対し、rbs-inline は生成しておらず、sord 削除のタイミングでその問題が健在化しました。

対応としては、以下のように @rbs! を使って直接 RBS をコードベース上に手書きしました。

# @rbs!
#   class Foo
#     attr_reader bar: String
#     attr_reader baz: Integer
#   end

# @rbs skip
Foo = Data.define(:bar, :baz)

さすがにこれはちょっと辛いですという話を soutaro さんにしたところ、rbs-inline 0.6.0 で Data, Struct がサポートされました 🎉

github.com

Data, Struct が面倒なことをsoutaroさんに共有している図

Data, Struct がサポートされたことによって @rbs! を使う必要がなくなり、以下のようにシュッと書けるようになりました。めっちゃ便利…!

Foo = Data.define(
  :bar, #: String
  :baz #: Integer
)

rbs subtract をやめる

sord gem が削除されたことでアプリケーションコードから RBS を自動生成する方法が rbs-rails と rbs-inline のみになりました。rbs-rails は Rails 側が自動的に生成するメソッドに対して RBS 生成を行うのが目的で、rbs-inline は開発者が実装したメソッドに対して RBS 生成するのが目的です。

それぞれ RBS の生成対象が異なることから重複を吸収する必要はないだろうと考え、 rbs subtract をやめました。

rbs subtract をやめてみたところ、以下のコードでエラーが出るようになりました。

class User < ApplicationRecord
  has_one :profile
  
  after_create :create_profile
  
  private
  
  # create_profile メソッドの型定義が重複しているエラーが発生
  def create_profile
    ...
  end
end

ActiveRecord はアソシエーションで関連付けたモデルに対して create_xxx メソッドを動的に定義します。動的に定義されたメソッドとアプリケーションコードで定義したメソッドによる型定義が重複したことによるエラーでした。今回のケースでは意図的にメソッドの上書きをしていたわけではなかったため、本来は別名をつけることが望ましいパターンでした。

rbs subtract をやめたことで、意図せずメソッドを上書きしていた場合はエラーによって別名で定義できるようになりますし、意図的にメソッドを上書きしていた場合は @rbs override を記載することでその意図をコード上に残せるようになります。

必要なくなったから rbs subtract をやめようくらいの気持ちで消していましたが、これは思いがけない発見でした。

さらに細かい話になりますが、上記の重複エラーの参照先が rbs-inline ではなく rbs-rails 側になっていることに気付きました。なんでだろうと思って調べてみると、rbs-inline は sig/rbs_inline/ に格納していて rbs-rails は sig/rbs_rails/ に格納していたのですが、RBS はアルファベット順でファイルを読み込んでいくため sig/rbs_inline/ → sig/rbs_rails/ という順番に RBS を読み込んでいたことに起因するものでした。

なので、rbs-inline は sig/rbs_inline ディレクトリではなく、sig/z_rbs_inline ディレクトリに格納するように変更し、必ず rbs-rails の後に読み込まれるようにしました。

これらの取り組みによって、最終的に RBS 生成のステップが以下のようにシンプルになりました。

Before

  1. rbs-inline で全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成
  3. sord で YARD から型を生成
  4. rbs subtract で 1. で生成された型に対して重複した型定義を除外

After

  1. rbs-inline で全体の型を生成
  2. rbs_rails:all で Rails によって生成されたメソッドの型を生成

今後の展望

やりたいと思っているものの、やりきれていないいくつかの点について共有します。

型検査を通す

今回の取り組みで rbs-inline を書いていく土壌は整いましたし、実際に書かれるようになってきましたが、型検査(steep check)を通すところまでは進められていません。これから始まる長い旅のスタート地点に立ったかなという気持ちです。

また、Rails プロジェクトに対して全てのディレクトリに対して型検査を通すようにすべきなのか、それとも特定のディレクトリだけで実施するのが妥当なのかの整理はできていません。これから検証含め進めていく必要があります。

リアルタイムな実装へのフィードバック

前編「RBS 活用推進の背景」で説明したように、実装のフィードバックサイクルを早めるためには rbs-inline のコメントを更新したらリアルタイムに RBS に反映され、その RBS を元にした型検査がエディタ上ですぐに走るのが理想だと考えています。

上記の型検査を通すだけでは理想の状況は実現できません。コーディング環境のセットアップを含む包括的な開発環境の提供を推進していく必要があります。

まとめ

RBS 導入の背景から YARD から rbs-inline への移行理由、移行方法、これからの展望について紹介しました。rbs-inline は experimental ではあるものの本番運用している会社がある事実があなたの会社 rbs-inline 導入への後押しになれば幸いです。

この辺りについてもっと話したい方はカジュアル面談でお話ししましょう!

product-recruit.timee.co.jp

前編:YARD から rbs-inline に移行しました

タイミーでバックエンドのテックリードをしている新谷(@euglena1215)です。

タイミーのバックエンドはモノリスの Rails を中心に構成されています。そのモノリスな Rails に書かれていた YARD を rbs-inline に一通り移行した事例を紹介します。
前編では、rbs-inline の紹介と rbs-inline への移行理由について触れ、後編では実際の移行の流れや詰まったポイント、今後の展望について触れる予定です。

rbs-inline とは

まずは rbs-inline について簡単に紹介します。

rbs-inline とは Ruby コードにコメントの形式で RBS を記述することで、対応する RBS ファイルを自動生成してくれるツールです。

github.com

README にあるサンプルコードを引用するだけになってしまいますが、以下の Ruby コードに対して rbs-inline コマンドを実行すると

# rbs_inline: enabled

class Person
  attr_reader :name #: String

  attr_reader :addresses #: Array[String]

  # @rbs name: String
  # @rbs addresses: Array[String]
  # @rbs return: void
  def initialize(name:, addresses:)
    @name = name
    @addresses = addresses
  end

  def to_s #: String
    "Person(name = #{name}, addresses = #{addresses.join(", ")})"
  end

  # @rbs &block: (String) -> void
  def each_address(&block) #:: void
    addresses.each(&block)
  end
end

以下の RBS ファイルが生成されるようになっています。

class Person
  attr_reader name: String

  attr_reader addresses: Array[String]

  def initialize: (name: String, addresses: Array[String]) -> void

  def to_s: () -> String

  def each_address: () { (String) -> void } -> void
end

サポートしている構文はこちらにまとまっています。
Syntax guide · soutaro/rbs-inline Wiki · GitHub

rbs-inline が作られた動機に関しては RubyKaigi 2024 での発表スライドを見てもらうのが一番かなと思います。

speakerdeck.com

RBS 活用推進の背景

まずは rbs-inline の前段階である RBS 活用推進の背景について紹介します。

メルカリがメルカリハロをリリースし、リクルートもスポットワーク業界への参入を表明するなどタイミーを取り巻く環境は激化の一途をたどっています。まさに戦国時代です。競合サービスと切磋琢磨し勝ち抜いていくために我々は1段階ギアを上げた開発をしていく必要があります。

そして、開発速度を高める方法はいくつかありますが、その中でも実装のフィードバックサイクルの高速化は良いアイデアの1つだと考えています。

タイミーを含む一般的な Rails アプリケーションの開発では、実装の検証はテストコードを用いた自動テストもしくは開発・検証環境での手動テストがほとんどなのではないかと思います。手動テストに一定の時間がかかるのは当然として、自動テストもそこそこのサイズの Rails アプリケーションでは数秒かかることは少なくありません。

それが仮にエディタ上でリアルタイムに静的な型検査という形でフィードバックが返ってくるとなるとどうでしょうか。もちろん手動・自動テストほど詳細なロジックミスは検知できませんが、多くのミスはちょっとした NoMethodError など型検査で気付けるものが大半です。

これまで数秒かけて気付いていたことにリアルタイムで気付けるようになれば、開発速度の改善には間違いなく寄与するはずです。

また、前提としてタイミーは元々 YARD コメントを書く文化があり、YARD コメントから RBS を生成する sord gem を使って RBS を補完用途で導入していました。詳しくは以下の資料をご覧ください。

tech.timee.co.jp

移行理由

RBS を活用し、型検査をしていくぞ!というのは前述の通りです。
一方、sord gem を使うことで YARD から RBS の生成はできていました。それでも rbs-inline に移行した理由は以下の通りです。

1. YARD(sord) よりも rbs-inline の方が表現力が高い

YARD(sord) では interface や type などの表現ができません。rbs-inline では@rbs! を使えば記述できます。

# RBS
class X
    type name = String | Symbol
    def foo: () -> name
end

# Ruby with YARD
class X
    # (String | Symbol) は表現できるが type を使った alias は表現できない
    # @return [String, Symbol]
    def foo = ['foo', :foo].sample
end

# Ruby with rbs-inline
class X
    # @rbs!
    #   type name = String | Symbol

    # @rbs () -> name
    def foo = ['foo', :foo].sample
end

2. YARD は書いていたが yardoc は使っていなかった

これは社内の事情ですが、YARD をコードリーディングを手助けするドキュメンテーションツールとしてしか使っておらず yardoc を用いてドキュメントページの生成はしていませんでした。(正確には一時期生成していましたが、誰も見ていなかったので生成をストップしました。)

同様の YARD を使っているプロジェクトであっても、yardoc を活用しているプロジェクトは yardoc 相当の挙動を rbs-inline で実現するツールを自作するか、yardoc によるドキュメント生成を諦めるかの判断を迫られることになります。

3. rbs-inline が今後言語標準の機能になっていく

rbs-inline gem は今後廃止されて rbs gem に統合される予定です。
rbs gem は Ruby 標準の機能なので rbs-inline も Ruby 標準の機能になるはずです。なので、今のうちに乗り換えておいて損はないだろう、という魂胆です。

また、開発者が社内にいる soutaro さんというのも大きなポイントでした。

productpr.timee.co.jp

rbs-inline はまだ experimental なので仕様が不安定という側面はあるものの、逆に考えるとフィードバックをすれば受け入れてもらう可能性が高いということでもあります。標準機能になるのなら、社内にいる soutaro さんに今のうちにフィードバックしておくことで我々のユースケースで困りにくい形で仕様が確定するといいなと思っています。*1

 


 

前編では rbs-inline の紹介、移行の目的などを紹介しました。このまま肝心の「実際どうだったのか」もお伝えしたいところですが、長くなったので一旦ここで一区切り。後編では実際の移行の流れや詰まったポイント、今後の展望をまとめます。お楽しみに!

今回は RBS 活用によって開発速度を向上させる作戦を取りましたが、開発速度向上には色んな方法があると思っています。各社がどんなアプローチで取り組まれているのかはとても興味があります。カジュアル面談でお待ちしています!

product-recruit.timee.co.jp

追記:後編を公開しました。

tech.timee.co.jp

*1:事実として我々のフィードバックによってバグ修正や新たな構文サポートが行われました。詳しくは後編で紹介します

スクラムで品質を上げ続けるために完成の定義(Definition of Done)を作りました

読んで欲しいと思っている人

  • POやステークホルダーと品質について共通言語や目標が欲しい開発者
  • 開発者と品質について共通言語や目標が欲しいPO
  • スクラムで品質について困っている人

読むとわかること

  • 完成の定義(Definition of Done)とはどんなものか
  • スクラムと非機能的な品質の関係性
  • タイミーのWorkingRelationsSquadでどんな完成の定義を作り、活用していきたいと思っているか
続きを読む

Vertex AI PipelinesとCloud Run jobsを使って機械学習バッチ予測とA/Bテストをシンプルに実現した話

こんにちは、タイミーでデータサイエンティストとして働いている小栗です。

今回は、機械学習バッチ予測およびA/BテストをVertex AI PipelinesとCloud Run jobsを使ってシンプルに実現した話をご紹介します。

経緯

タイミーのサービスのユーザーは2種類に大別されます。お仕事内容を掲載して働く人を募集する「事業者」と、お仕事に申し込んで働く「働き手」です。

今回、事業者を対象に機械学習を用いた予測を行い、予測結果を元にWebアプリケーション上で特定の処理を行う機能を開発することになりました。

要件としては以下を実現する必要がありました。

  1. 定期的なバッチ処理でのMLモデルの学習・予測
  2. MLモデルのA/Bテスト

最終的に、Vertex AI PipelinesとCloud Run jobsを活用したシンプルな構成でバッチ予測とA/Bテストを実現することにしました。

本記事では主に構成とA/B割り当ての仕組みをご紹介します。

構成

まず、全体構成とその構成要素についてご紹介します。

構成図

Webアプリケーション側の構成・実装についてもご紹介したいところですが、今回は機械学習に関係する部分に絞ってお話しします。

前提として、データサイエンス(以下DS)グループはGoogle CloudをベースとしたML基盤を構築しています。MLパイプライン等はCloud Composerに載せて統一的に管理しており、今回も例に漏れずワークフロー管理ツールとして採用しています。

MLパイプラインはVertex AI Pipelinesで実装しています。MLモデルのA/Bテストを実現するため、MLモデルごとにパイプラインを構築し、並行で稼働させています。同時に、それぞれのMLモデルの予測値と付随情報をBigQueryの予測結果テーブルに蓄積する責務もMLパイプラインに持たせています。

もちろんそれだけでは予測結果がテーブルに蓄積されるだけでA/Bテストは実現できないので、各事業者に対する予測結果のA/B割り当てをCloud Run jobsの責務とし、MLパイプライン実行の後段タスクとして実行しています。同時に、割り当て結果をBigQueryテーブルに出力する処理も実施します。

当初はA/B割り当てを含めたすべての責務をVertex AI Pipelinesに集約する案も議論の中で出たのですが、将来的に類似の取り組みにて実装や思想を使い回せそう等の理由から、取り回しのしやすいCloud Run jobsを採用しました。

Cloud Run jobsの利用はDSグループ内でも初めてではありましたが、グループ内のMLOpsエンジニアに相談・依頼してCloud Run jobs用CI/CDの導入などML基盤のアップデートを並行して進めていただくことで、スムーズに開発を進められました。

今回、モジュール間のデータやり取りのIFとしてBigQueryを採用していますが、読み込み・書き込みの操作に関しては、以前ご紹介した社内ライブラリを活用することでサクッと実装できました。

また、各処理には実行日時などの情報が必要なため、Cloud Composerのオペレータからパラメータを渡してキックする形にしています。

例えば、Cloud Run jobsは2023年のアップデートからジョブ構成のオーバーライドが可能になっており、それに併せてCloud Composer側でもCloudRunExecuteJobOperatorを介したオーバーライドが可能になったため、そちらを利用して必要なパラメータを実行時に渡しています。

さて、A/B割り当ての結果が出力されたのち、Webアプリケーション側はデータ連携用テーブルを参照して、事業者に対してバッチ処理を行います。残念ながら機能や施策の具体についてはご紹介できないのですが、機械学習の予測結果を元に事業者ごとに特定の処理を行う仕組みになっています。

A/B割り当ての仕組み

次に、Cloud Run jobsの中身で実施しているA/B割り当てについて、より具体的にご紹介します。

どう設定を管理するか

A/B割り当てに必要なパラメータはyamlファイルで指定する形にしています。例えば、実験期間や各MLモデルへの割り当て割合などです。

- experiment_name: str # 実験名。割り当てに用いるキーも兼ねる e.g. 'experiment_1'
  start_date: str # A/Bテストの開始日 e.g. '2024-08-01'
  end_date: str # A/Bテストの終了日 e.g. '2024-08-31'
  groups:
    - model_name: str # MLモデルの名前 e.g. 'model_1'
      weight: float # このMLモデルに割り当てる割合 e.g. 0.5
    - model_name: ...
- experiment_name: ...
  ...

この方法を用いる問題点として、”PyYAML”というライブラリを使えばyamlを読み込むこと自体は可能なのですが、開発者が想定していない形式でyamlが記述されるとエラーや予期せぬ挙動に繋がります。

当初の開発者以外がyamlファイルを更新することを見越して、ファイルの中身をバリデーションすることが望ましいと考えました。そこで、型・データのバリデーションが可能なライブラリである”Pydantic”を活用することにしました。

上記の形式のyamlファイルを安全にパースするために、以下のようなPydanticモデルクラスを定義しています。

# コードの一部を抜粋・簡略化して記載しています

import datetime

from pydantic import BaseModel, Field, field_validator

class ABTestGroup(BaseModel):
    model_name: str
    weight: float = Field(..., ge=0.0, le=1.0)

class ABTestExperiment(BaseModel):
    experiment_name: str
    start_date: datetime.date
    end_date: datetime.date
    groups: list[ABTestGroup]

    @field_validator('groups')
    @classmethod
    def validate_total_weight(cls, v: list[ABTestGroup]) -> list[ABTestGroup]:
        """
        各groupのweightの合計が1.0であることを確認する。
        """
        total_weight = sum(group.weight for group in v)
        if not math.isclose(total_weight, 1.0, rel_tol=1e-9):
            raise ValueError('Total weight of groups must be 1.0')
        return v

class ABTestConfig(BaseModel):
    experiments: list[ABTestExperiment]

例えば、weight(各グループに割り当てる事業者の割合)に対しては、型アノテーションとFieldを使って型と数値の範囲のバリデーションを実施しています。

加えて、各グループの割合の合計が1.0を超えることを避けるため、フィールドごとのカスタムバリデーションを定義可能なfield_validatorを使用し、独自のロジックでバリデーションを実装しました。

このようなPydanticを使ったバリデーション処理をML基盤のCIを通して呼び出すことにより、不適切なyamlファイルを事前に検知できるようにしています。

どう割り当てるか

A/B割り当てに関しては、事業者を適切にバケット(=グループ)に割り当てるために、事業者のIDとキーを使用しています。

# コードの一部を抜粋・簡略化して記載しています

import hashlib

def _compute_allocation_bucket(company_id: int, key: str, bucket_size: int) -> int:
    """
    company_idとkeyに基づき、company_idに対してバケットを割り当てる。
    keyはexperiment_nameなどを想定。
    """
    hash_key = key + str(company_id)
    hash_str = hashlib.sha256(hash_key.encode("utf-8")).hexdigest()
    return int(hash_str, 16) % bucket_size + 1

まず、実験名をkeyとして事業者のIDと結合し、ハッシュ化の元となる文字列を生成します。次に文字列をハッシュ化し、16進数の文字列を取得します。ハッシュ値を整数に戻した後、バケットサイズで割った余りをバケット番号とします。

その後、各MLモデルに対してweightに基づいてバケット範囲を割り当てることでMLモデルと事業者を紐付けます(コードは省略します)。

ややこしい点はありつつも上記のロジックにより、同じ事業者とキーの組み合わせに対して一貫して同じバケットを割り当てることができます。

実行のタイミングによって割り当てが変化する等の問題が生じず、A/Bテストの管理が容易になります。また、ハッシュ関数を使うことで入力値をほぼ均等に分散させることができ、実質的にランダムにグループ分けすることができます。*1

おわりに

機械学習バッチ予測とA/Bテストをシンプルに実現した話をご紹介しました。

今回、データサイエンティストとバックエンドエンジニアの共同開発については黎明期といった状況での開発でしたが(そこがまた楽しいのですが)、主にPdM含めた3人で協力しながら手探りで設計・実装を進めていきました。

その他にもMLOpsエンジニア、データエンジニアなど多くのポジションの方々に協力いただいており、部門を跨いでスムーズに協業できる組織体制が整ってきたことを開発を通して感じました。

今回ご紹介した構成にはやや課題が残っていたりするのですが、部門を横断しつつ解決を図っていけるのではと考えています。

We’re Hiring!

タイミーのデータエンジニアリング部・データアナリティクス部では、ともに働くメンバーを募集しています!!

現在募集中のポジションはこちらです!*2

「話を聞きたい」と思われた方は、是非一度カジュアル面談でお話ししましょう!

*1:ハッシュ関数を活用したA/B割り当てに関してはGunosyさんのブログ記事が分かりやすいです。

*2:募集中のエンジニア系のポジションはこちらです!