シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

「ふりかえりカタログ」の振り返り手法をチームで実践してみました

こんにちは、開発部会員企画開発チームの日比野です。

今回は、私たちのチームで導入したいくつかの振り返り手法についてご紹介します。

背景

私たちのチームでは週次で KPT(Keep, Problem, Try) を用いた振り返りを行っていましたが、次第に意見が出にくくなり、チームとして取り組むべき課題も挙がりにくくなるという問題を抱えていました。

この状況を改善するため、私たちは新しい振り返りの手法を試してみることにしました。

ふりかえりカタログ

「何か新しい振り返りのやり方はないか?」と探していたときに見つけたのが、こちらの「ふりかえりカタログ」です。

qiita.com

このサイトには目的や状況に応じた様々な振り返りの手法がまとめられているので、この中から活用できそうな振り返り手法を選びました。

週次:感情グラフ + タイムラインのハピネスレーダー

私たちは、週次の振り返りの KPT で意見が出にくくなっていた原因を「そもそもその週にあった出来事を全員が思い出せていないのでは?」と考え、「出来事を思い出す手法」の中から新しいやり方を探すことにしました。

チームで検討した結果、週次の振り返りでは「感情グラフ」と「タイムラインのハピネスレーダー」を組み合わせた手法を導入しました。

具体的なやり方

  1. Miro のホワイトボードツール上に、横軸を曜日、縦軸を「🙂」「😐」「☹️」の3段階の感情としたグラフを用意します。
  2. チームメンバーは日々の業務の中で、何か出来事があったら付箋に内容を書き、その時の感情に合う位置に貼り付けます。
  3. 週次の振り返り会の時間で、その週に貼られた付箋の中から、特に話したいものや KPT の K(Keep) や P(Problem) に繋がりそうなものをピックアップし、全員で深掘りして話します。

やってみてどうだったか

KPT では意見として挙げるほどではないけれど、少しモヤっとした、あるいは嬉しかった、といった細かな感情に紐づく出来事をチームで共有できるようになりました。
今はまだ個々の出来事の共有に留まるものも多いですが、今後はこれをチーム全体の改善アクションに繋げていけるよう、運用を継続していきたいと考えています。

月次:象、腐った魚、嘔吐

少しインパクトのある名前ですが、これはチームに潜む「見て見ぬふりをされている問題」を話し合うための手法となっています。
実践してみたい振り返り手法として挙がったのですが、頻繁に意見が溜まっていくようなものではないため、出てきた意見は月次で確認することにして導入しました。

具体的なやり方

以下の3つの観点で、気になっているけれど普段は言い出しにくいことを付箋に書き出します。

  • 象🐘
    • 誰もがその存在に気づいているのに、あえて口に出さない(見て見ぬふりをしている)大きな問題や真実を指します。
  • 腐った魚🐟
    • 早く処理しないとどんどん臭くなっていく腐った魚のように、隠している悩みや過去の過ちを指します。
    • 早く打ち明けた方がよいけれど、なかなか言い出せないことです。
  • 嘔吐🤮
    • 普段は胸の内にしまっている不満や意見を、批判される心配なくすべて吐き出すことです。

月次の振り返りでは、これらの観点で集まった付箋について、心理的安全性を確保した状態で話し合います。

やってみてどうだったか

普段の振り返りでは議題に上がりにくい根深い問題やチームに対する本音を共有する場にできています。
テーマの性質上暗い雰囲気になりがちで、活発に意見を言い合って結論を出すということが難しい議題も多いため、一旦はチーム全体で共通認識を持つというところまでで留めておくことにしています。

オフライン:焚き火 + 闇鍋

振り返りの話からは逸れてしまいますが、ふりかえりカタログにある手法を用いたオフライン MTG も行ったのでそちらについても紹介します。
私たちのチームでは、定期的にオフラインで集まる機会も設けていて、その場ではチームのコミュニケーションをより深めることを目的としています。
そこでふりかえりカタログの「アイデアを出し合う手法」からヒントを得て、「焚き火」と「闇鍋」を組み合わせたワークを行いました。

具体的なやり方

  1. モニターに焚き火の映像と音を流し、リラックスした雰囲気を作ります。
  2. 各々が業務の話や趣味の話などに関連するテーマを付箋に書き、「闇鍋」のように一つの場所へ集めます。
  3. ランダムに付箋を1枚引き、そこに書かれたテーマについて全員で自由に雑談します。

やってみてどうだったか

この時に出たテーマとしては、「みんなはAIをどう活用している?」「業務で使っている便利なツール教えて!」などがありました。
普段はチームで雑談をする時間を設けていないため、知見を共有し、チームメンバーの普段の様子を知ることのできる有意義な時間になりました。
焚き火については、オフラインでの MTG なので周りにある程度の雑音もあり、MTG もチーム全員で行ったため無言になることも少なく、特に大きな効果はなかった印象でした。シーンとした空気になりやすい MTG の時に利用する方がメリットを感じられそうです。

まとめ

今回は、私たちのチームで試した振り返りの手法について紹介しました。

振り返りの目的を意識し、チームの状態に合わせて手法を柔軟に変えていくことで、振り返りのマンネリ化を防ぎ、継続的な改善に繋げることができると感じています。

もしあなたのチームでも同じように振り返りの課題を感じていたら、「ふりかえりカタログ」をヒントに、何か新しい手法を試してみてはいかがでしょうか。

RubyKaigi2025に参加した弊社メンバーで感想を話し合いました

開発部・アプリ基盤チームの深野です。
今回、弊社から自分を含む3名が松山で開催されたRubyKaigi2025に参加して色々なセッションを聞いてきました。
その3名で印象に残ったセッションのことや参加しての感想などを話し合ったものを文字起こしして、再構成したものが今回のブログになります。
なお、弊社はSilver SponsorとしてRubyKaigi2025に協賛させていただきました。

自己紹介

竹内: シンクロフード3年目の竹内と申します。普段は主にモビマルというキッチンカー関連のサービスの開発を行っています。Rubyに関しては、高校生くらいの時からやっていて、もう20年くらい前から触れています。RubyKaigiに関しては、今回が初参加となります。よろしくお願いします。

小島: 去年新卒で入社して今年で2年目の小島です。Ruby自体は入社前に1年内定者インターンで利用していたので今年でRuby歴3年目に入りました。3年目ですがRubyに関してはまだまだ初心者です。RubyKaigiの参加は初めてで、東京Ruby会議に今年の1月に参加しています。よろしくお願いします。

深野: 新卒でシンクロフードに就職してから、現在6年目になる深野です。Ruby歴は5年ほどです。RubyKaigiには、去年から続けて2回目の参加になります。RubyKaigi以外の技術カンファレンスですと、Kaigi On Railsに去年参加しました。よろしくお願いします。

道後温泉の近くの愛媛県民文化会館という大きな会場をほぼ貸し切ってイベントは行われていました

印象に残ったセッション

Make Parsers Compatible Using Automata Learning

深野:竹内さんにまずは聞いてみます。今回RubyKaigiに参加した中で印象的だったセッションを1つ教えてください。
竹内: 僕はパーサーに興味を持っていたので、1日目の「Make Parsers Compatible Using Automata Learning」というセッションに興味を持ちました。
深野: そのセッションは具体的にどのような内容でしたか?
竹内: Rubyのパーサーにはparse.yとPrismという2つがあるのですが、その互換性を形式的な手法を使って違いがないかを確認するという手法の紹介でした。

小島: そもそも普段Rubyを書いていてパーサーが気になることってあまりない気がするのですが、なぜそもそもパーサーに興味があったのでしょうか?
竹内: そうですね。昔から言語処理系などを作っていたので、その時パーサーの手法などを色々調べていたという感じです。
小島: なるほど。昔から言語処理系を作っていたんですね。

小島: 深野さんも何か質問はありますか?
深野: 具体的にどのようにその2つのパーサーに互換性があるということを示したのでしょうか?
竹内: Automata Learningという手法が用いられています。ブラックボックスになっているパーサーを外からモデル化して、その内部構造を自動的に推定するという手法になっています。言語全体をモデル化するのは難しいのですが、その一部に着目することで違いを見つけていくことは可能だそうです。
深野: なるほど。ブラックボックスということはCやRubyなどで書かれたパーサーを静的解析したりするのではないのでしょうか?
竹内: 静的解析というよりは、ブラックボックスとしてパーサーを実際に実行した振る舞いから分析する手法となっています。そのため他の言語で書かれていてもこの手法を用いることが可能だと思います。
深野: なるほど。振る舞いの方を見るということですね。

深野:内容はなんとなくわかったのですが、どこが面白いと感じたのでしょうか。
竹内: そうですね。パーサーの振る舞いからオートマトンを形成するというアルゴリズムなのですが、一般的なRubyなどの文法では正規文法には収まりません。そのためどのようにオートマトンを作成していくのかが気になりました。正規言語ではないのでDFAなどでは扱えませんが、VPA(Visibly Pushdown Automata)と呼ばれる、正規言語より強い言語を表現できるオートマトンを使うというのが興味深かったです。文脈自由文法よりは弱いので全ては表現できないんですけども、今回の用途に実用できるほどの表現力を持ったオートマトンということで、興味を持ちました。
深野: オートマトンの授業で習う初歩的なものよりは広いけど文脈自由文法まではカバーできないオートマトンがあって、それを使ってパーサーの動きをオートマトンで表現したということなんですね。こんなことができるとは知らなかったです。
竹内: 一般的なオートマトンの授業で習うのがDFAやNFAだと思うんですけど、その次にプッシュダウンオートマトンと言って文脈自由文法を表現できるオートマトンの一種があって、VPAはその中間に位置するものですね。これだと、DFAのように共通部分や差分などを計算できつつ、DFAよりも強いという性質を持つ文法になっているそうです。

Ruby Taught Me About Encoding Under the Hood

深野: では、小島さんはRubyKaigi初参加だと思うのですが、面白かった講演はどちらでしょうか?
小島: 僕が印象に残っているのは初日のキーノートでima1zumiさんという方がお話されていた「Ruby Taught Me About Encoding Under the Hood」という、文字コードエンコーディングの発表です。
深野: 私と竹内さんは内容を知ってはいるのですが、ご存知ない方のために講演内容を教えてください。
小島: 1時間の発表なので内容が結構あるんですけど、最初に文字エンコーディングの歴史を手早く紹介して、その後、エンコーディングのハマりやすいところについて実体験を交えながら発表されていました。最後は、今後Rubyで文字コードエンコーディング関係のどういう改善をしたいかっていう話をしていただきました。

深野: 面白かった点は何かありますか?
小島: そうですね、僕はRubyKaigiに参加しているのにそんなにRubyに詳しくなかったのですが、Rubyに詳しくなくても話を通してわかるというのがまず助かったポイントではあります。面白かった点としては、半角カタカナがなぜ何のために生まれたかみたいな話だったり、普通のひらがなとかだと2バイト以上使っていると思うのですが、そういう文字の内部での扱いの話が面白かったです。他には、書記素クラスタという、僕ら人間が見た時の文字数みたいな概念があるらしいのですが、その話も面白かったです。
深野: ありがとうございます。そうですよね。私も書記素クラスタの概念などは知らなかったので勉強になりました。家族の絵文字が表示されるのは1文字なんですけど、内部的には4つのコードポイントで構成されているみたいな内容ですよね?
小島: 先ほど発表を見直したら7コードポイントでしたね、内部的には。
深野: あ、7つ。結合文字(Zero Width Joiner)があるからですね。内部的に7つのコードポイントを結合して、幅的には1文字の家族の文字ができてるみたいなのは知らなかったです。

「Å」や「び」のように単一のコードポイントでも表せるものと、🧑‍🧑‍🧒‍🧒のように7つのコードポイントでしか表せないものがあるというのも面白い点でした

竹内: 僕の興味を惹かれた点を言うと、Unicode15.1.0で追加された Indic Conjunct Break プロパティがあります。昔サンスクリットと一緒にデーヴァナーガリーを学んだ際に、文字の結合がかなり複雑になっていて苦労したのを思い出しました。例えば क् と ष が連続すると क्ष という全く見た目が異なる文字になったりします。
深野: なるほど。文字って奥が深いですよね。私はアルファベットと日本語の文字ぐらいしか詳しくないですけど、世界の他の国を見ていけばなんか違うルールだったり違う体系の文字がありますよね。西洋のソフトウェアのアスキー文字以外への解像度の低さに苦しめられてきた日本人としては、世界の多様な文字をソフトウェアでサポートしていってくれるUnicodeの新しいバージョンには積極的に追従していきたいですね。

On-the-fly Suggestions of Rewriting Method Deprecations

竹内: それでは深野さんは、今回のRubyKaigiで興味を持ったセッションはありましたでしょうか?
深野: 「On-the-fly Suggestions of Rewriting Method Deprecations」という講演が面白かったです。
竹内: 内容についてはどのようなものだったでしょうか?
深野: はい。こちらの内容は、プログラミングやライブラリのDeprecation Warning、非推奨警告の対応方法について整理した上で、Rubyについて今のシステムよりももっと自動化されたようなものを提案しているという内容です。具体的には、まずDeprecation Warningに関して、既存のプログラミング言語がどういった方法で対応しているのかをそれぞれ列挙した上で、それらの対応方法を自動化具合のレベル別に5段階でまとめています。著者はこのように5段階で表現した中で、Rubyで5段階目の自動化のその先まで考えようという内容になっています。

小島: 僕もこのセッションを聞いたのですが、ちょっと結末を覚えていなくて、Rubyで結局どうしようみたいな結末になったのでしょうか?
深野: 結末としては、発表者はPharoというプログラミング言語を参考にして、Deprecation Warningのランタイムでの自動修正ができるようなgemを作ったんですが、実際それにはまだかなり制約があるということでした。技術的な制約がまずありますが、それよりさらに大きいものが、Rubyのエコシステムがこれをそもそも採用するのかというところですね。現状、これは単なるサードパーティgemであって、言語の組み込み機能ではないですよね。言語の組み込み機能としてDeprecation Warningの書き換えをサポートしているようなものはC#などがありますが、Rubyはそうではありません。この機能を使うには結局gemの作者がこのgemを認識してもらうとか、逆にgemを使うユーザー側が自分でDeprecation Warningの書き換えルールを自分で書いて設定する必要があるという意味で、提案はしたけど、実際はまだまだ難しいんじゃないかという結論だったかなと思います。
小島: なるほど難しいのですね。ただ、個人的にはランタイムでの自動修正があったら嬉しいなと思うのですが、深野さんはどうでしょうか?
深野: はい。嬉しいなと思います。今は弊社ではDeprecation WarningについてはランタイムでSentryに通知して、それを見て手動でプルリクを作って直すということをやっているんですけれども、やはりどうしても手間がかかります。ランタイムでの自動修正みたいなところができれば、言語だったりフレームワークだったり、gemだったりのバージョンアップが全体的にもっとスムーズに行えるようになると思います。

竹内: 聞いていて興味深いなと思いました。今回の発表というのは、コードを実行した際にDeprecatedになっていて、変更すべきコードを自動的に書き換えてくれるという認識でいいでしょうか?
深野: そうですね。コードの実行時に書き換わるというような想定なんですけれども、もちろんこの機能はproduction環境での実行というのは考慮されていないようです。test環境だったり、あとはローカルのdevelopment環境などでコードを実行して、その時に書き換えるというようなのを想定しているという話だったと思います。
竹内: ありがとうございます。もし完全にできたらかなり有益だと思うんですが、主な技術的な制限というのはどのようなものがあるのでしょうか?
深野: そうですね。例えば、まず書き換えのルールとして単純なメソッドの書き換えというのはすごく簡単ですよね。例えばRubyのURIでしたら、URI.regexpが非推奨になったので代わりにURI::DEFAULT_PARSER.make_regexpを使うみたいにするというのはすごく簡単だと思います。ただし代替策がないような場合、つまりこのメソッドが使えなくなりますが、これと同じような処理をする代替メソッドが提供されたりしていないみたいなケースでは書き換えルールを設定するのがとても難しそうですし、発表時点のRubyのDepreWriterでは実装されていなかったようです。
竹内: ありがとうございます。確かに代替策がない場合とかコンテキストによってどう書き換えるかが変わる場合というのはありそうなので、完璧に実装するのは難しそうだなというのは理解できました。

Matz Keynote

深野: ところで、今回のMatzさんのキーノートについてはどのような感想を持ちましたか?
竹内: そうですね、私が特に印象に残ったのは1番最初に話されていたReverse Alpha Syndromeの話ですね。AIが発展していくにつれて、人間はAIのために働くようになってしまうのではないかという懸念についてです。確かに注意していかないと、人間が下僕になってしまうというのは確かにその通りかもしれないと思いますね。最近のAIエージェントによるコーディングを見ても少しそうなってきている感覚はあるので、プログラミングの楽しさを見失わない、忘れないようにしていくのは1つ重要なことかなと思いました。小島さんは何か感想などありますか?
小島: そうですね。今竹内さんが言ってくださったのは僕も同感です。後はそうですね、正直僕はMatzさんがRubyコミュニティにおいて何をしているかそんなに詳しく知らなかったのですが、キーノートの内容を聞いて、あ、この人がRubyのコミュニティのトップなんだなっていう感じがすごくしました。Rubyの将来まで見据えられていて面白い内容でした。
深野: 私は型についての話が印象的でした。最近の静的型付け言語ブームでは開発生産性や安全性などよりも、実は1番理由として大きいのは開発者が型のパズル的な側面が楽しいからなんじゃないかという大胆な話だったのですが、自分は結構共感してしまいました。実際にrbsを触っていたりするのはかなり楽しいですし。MatzさんはRubyに言語機能としての静的型付けを組み込むことに反対されているとは思うのですが、静的型づけ言語を触る楽しさの部分を認めてくれたのはいいなって思いました。
竹内: 最後にもう1点いいでしょうか。他に印象深かったのが、Rubyコミッターと、会場に来ている、セッションを聞いている人たちがそれほど壁がないという発言でした。ちょっとした巡り合わせやエネルギーの向き方で周囲に与える影響も大きく変わってきますし、小さなことでもRubyコミュニティに貢献できたりはしますので、そうした垣根っていうのはそれほど大きくないんだなっていうのは、Matzさんのメッセージの中で感銘を受けた部分です。
深野: 確かにそこはすごく重要なメッセージでしたね。

RubyKaigi全体についての感想

深野: 最後にRubyKaigiに参加しての感想を一言ずつ述べていきましょう。
竹内: 今回がRubyKaigiの初参加でした。10年くらい前はRubyコミュニティの動向を追っていたんですが、最近は離れている状態でした。今回参加して忘れていたRubyの楽しさを思い出せたのが非常に良かった点だと思います。小島さんは何か感想ありますか?
小島: 僕はRubyにそんな詳しくなくてそれでも楽しめるのかなって少し不安だったのですが、先ほど紹介したima1zumiさんのキーノートの内容もRubyをそんな深く知らなくても楽しめる内容でしたし、そういう発表がいくつかありました。あとはやっぱりRubyのコミュニティに触れるいい機会になるというか、参加したことでRubyを勉強するモチベーションも上がったり、色々いい影響を受けることができたので参加して良かったなと思ってます。深野さんはどうですか?
深野: 今回のRubyKaigiの感想というか、去年のRubyKaigiに参加してからの1年の感想になってしまうんですけど、同じ人の発表を連続して聞き続けるとその人の興味の変遷だったり、OSSを公開している方だったらその成果物だったりを追いかけられて楽しいというところに気がつけました。具体的には、Rubyで実装されたWebアプリケーションの高速化について、去年は速度の計測について話していたのが今年はそこから発展して実際にSinatraを改造して速くするという発表をされていたosyouyuさんの発表などです。去年のRubyKaigi初参加もとても面白かったのですが、それとはまた異なるRubyKaigiを縦で追い続ける楽しみというのを味わうことができました。

今回は発表の話ばかりでしたが、各ブースでそれぞれの企業の事業内容や開発環境を聞くのも楽しめました。

おわりに

一緒に参加した弊社メンバーと講演についての感想を話してみることで、自分が聞けなかった講演についても興味を深めることができたり、自分が聞いていた講演についても改めて内容を咀嚼したりすることができました。
RubyKaigiのオーガナイザーの方々や、発表者の皆さんへの感謝でこの記事を締めたいと思います。
来年のRuby Kaigiも非常に楽しみです。

来年はついに北海道!

本番環境 DB の個人情報マスキングフローを Jenkins から StepFunctions に移行しました

初めまして、SRE チームの 辻井 です。

先日、本番環境 DB の個人情報マスキングフローを Jenkins から Step Functions に移行しました。
今回はその対応や検討の経緯を紹介させていただきたいと思います。

背景

本番環境 DB の個人情報マスキングフローでは、最初に本番環境 DB のクローンを作成し、マスキングした後に社内公開しています。

(なお、本稿では上記フローで作成する DB を クローン DB と呼称させて頂きます。)

そもそもクローン DB は何を目的に作成している?

クローン DB が作られている理由は2つあります。

理由1: サービス指標などの分析のため、社内向けに本番環境に近いデータを公開したい

本番環境の DB は個人情報や機密情報を含むため、限られた社員しか閲覧権限がありません。
しかし、各部署で意思決定のために確認したいデータがあります。

例えば、会員登録数、問い合わせ数、応募数などの指標です。

そんな要望に応えるため、マスキング処理済みの クローン DB を社内向けに公開しています。

理由2: テスト環境 DB をなるべく本番環境 DB に近い状態にしたい

テスト環境でのテストの信頼性を高めるためです。

例えば特定の DB テーブル定義に変更を加えたいとして、本番環境での変更時にどれほど実行時間がかかるかを事前にテスト環境で計測する際、なるべく本番環境に近いデータである方がよりテスト結果の信頼性が高くなります。

クローン DB はマスキング後に mysqldump した結果を dump ファイルとして S3 に保存しているため、任意にテスト環境 DB にインポートし活用できるようにしています。

このように、クローン DB は重要な役割を担っています。

どう作られていたか?

クローン DB はバッチ処理にて日次作成しており、朝 07:00 ごろに作成し、23:00 に削除しています。
バッチ処理のワークフロー管理には Jenkins Pipeline を使っていました。

処理の流れについては以下の通りです。

どんな課題があった?

従来の クローン DB 作成フローでは、以下の3つ課題がありました。

  • 1. ジョブが失敗するとリカバリーがしづらい

    • ジョブが失敗した場合は Jenkins Pipeline 上で失敗した地点のジョブから全て手動実行し直す必要がありました。
  • 2. 実行時間が長い

    • 全体の実行時間が5時間近くかかっていました。
      主な原因としてはマスキング処理や mysqldump のタスク処理量が多く、かつ直列実行されていたためです。
    • 通常作成時は問題にならないのですが、失敗した時の再実行に時間がかかり作成完了が夕方 16 時ごろと遅れてしまうことが最大の問題点でした。
  • 3. メンテナンス性が低い

    • テスト環境がない
      • いきなり本番環境でメンテナンスせざるを得ず、不安定な状況でした。
        メンテナンス頻度は低いものの、安全にメンテナンスできることが望ましいです。
    • コード管理できていない
      • Jenkins の Pipeline やジョブ
        • GUI 上から作成されており、コード管理できていませんでした。
      • Jenkins ジョブでの処理内容
        • スクリプトにベタ書きされており、変更履歴が管理されていませんでした。
          そのため、いつどんな理由で変更され、現在のコードになっているのか?が把握しづらい状況でした。

どう対応した?

大きな変更点として、タイトル通り Jenkins から Step Functions に移行しました。
処理の流れは以下の通りです。

Step Functions を採用した理由

以下の4点です。

  • 失敗した処理から容易に再実行可能
    • Step Functions では各タスクが個別のステートとして管理され、リドライブ により失敗したタスクから再実行が可能です。
  • 並列実行用のステート(Map)がある
    • 後述の通り、実行時間の課題解決のために Map ステートが使えそうだと考えました。
    • Map ステートとは、配列を渡すことで配列の要素ごとに同じ処理を繰り返してくれるステートです。今回では DB リストを配列で渡し、マスキング処理を DB ごとに繰り返し実行させています
  • 全て Terraform でコード管理可能
    • Step Functions を含めた必要リソースを Terraform で管理することで、ソースコード管理とバージョン管理が可能になります。
  • AWS サービスと相性がいい
    • 例えば RDS クローン時に RDS API: RestoreDBClusterToPointInTime を使っているのですが、このような API を Step Functions から実行可能だったりと便利です。

※補足: Step Functions 採用理由について
Jenkins Pipeline でも retry による再実行、parallel による並列処理実行は可能です。
ただ、今回はテスト環境構築と、Terraform と Docker によるコード管理も合わせて達成したかったため総合的に見て Step Functions を採用しています。


背景にあった 3つの課題に対しては、以下のように対応しました。

1. ジョブが失敗するとリカバリーがしづらい: Step Functions 採用

  • 上述の通り、Step Functions のリドライブ機能を活用し解決を図りました。
    • 下記のように AWS のマネジメントコンソールからボタン1つで実行可能で、シンプルです。

2. 実行時間が長い: 処理並列化

特に実行時間が長かった マスキング処理、 mysqldump 処理を並列化しました。

  • マスキング処理: Step Functions の Map ステート 採用

    • Map ステートで並列化しました。
      • 下記のように Lambda で取得した DB リストを Map ステートに渡し、Fargate 上で自社製のマスキングプログラム(Java) を対象 DB に対し実行しています。
  • mysqldump: GNU Parallel 採用

    • Fargate で実行する bash 内で GNU Parallel を利用し並列化しました。
    • Map ステートではなく GNU Parallel を使っている理由
      • 複数処理をまとめる上で都合が良かったからです。
        S3 へ dump ファイルを格納するには、mysqldump 実行 + dump ファイル送信と 2ステップ必要です。
        ならば、DB ごとに個別の Fargate コンテナで実行するより単一の巨大な Fargate コンテナでまとめた方が無駄なデータ受け渡しが発生せずスムーズで早いと判断しました。
    • ちなみに、実際に Parallel を使っている箇所は下記のような感じです。
    # parallel はサブシェルで実行されるため、必要な関数や変数を環境変数としてエクスポートして parallel に渡す
    export PARALLEL_HOME=/parallel
    export -f dump_table
    
    echo "Run with $MAX_JOBS parallel jobs"
    
    get_db_tables | while read -r db table; do
      if [ -z "$(echo "$IGNORE_TABLES" | grep "^$db\.$table\$")" ]; then
        echo "$db $table"
      fi
    done | parallel -j "$MAX_JOBS" --colsep ' ' dump_table

3. メンテナンス性が低い: テスト環境整備 + コード管理

  • テスト環境の整備
    テスト環境に Step Functions や必要なリソースを整備し、テストを可能にしました。
    クローン元とする DB 名などはパラメータで変更可能なので、任意の DB をマスキングテスト可能です。

  • Terraform と Docker によるコード管理
    AWS 上のリソース (Step Functions, Fargate, S3 など) は全て Terraform で、Fargate や Lambda 上で実行するスクリプトは Docker イメージにてコード管理しました。

改善効果

背景での課題について、以下の通りに解消することができました。

  • 1. ジョブが失敗するとリカバリーがしづらい

    • Step Functions 採用によりリカバリーが容易になり、ダウンタイム短縮と運用効率化を実現しました。
  • 2. 実行時間が長い

    • 全体の実行時間を約5時間から約3時間20分まで縮められました。
      • マスキング処理
        • 従来の実行時間は約2時間でしたが、約1時間10分まで短縮できました。
      • mysqldump
        • 従来の実行時間は2時間10分でしたが、約1時間に短縮できました。
  • 3. メンテナンス性が低い

    • テスト環境の整備
      テスト環境でのテストが可能となり、アップデート時のリスクを低減しました。
    • Terraform と Docker によるコード管理
      変更履歴が明確になり、環境構築の自動化と再現性が大幅に向上しました。
      特に Step Functions のワークフローと、マスキング等の処理内容をコード管理できたのが大きく、だいぶ管理が楽になりました。

学んだこと

テスト方法を最初から確立させておく

テスト方法をもう少し工夫すべきでした。

Step Functions 移行後も全体での実行時間が4時間弱ほどかかるため、そのまま実行してテストするとかなりの時間がかかります。待ち時間は別タスクを行うものの、コンテキストスイッチが都度入るのは良くありません。

テストしたい処理のみを実行するステートマシンをテスト用に用意するなどでテスト単位を細分化すべきでした。

一気に変更しすぎない

Step Functions への移行時にマスキング、mysqldump の並列化の実装を一気に盛り込みましたが、いざエラーが起きた際に調査範囲が広がり開発工数が伸びてしまいました。

今回は以下のように2ステップに分けるのがベターでした。

  1. Jenkins -> Step Functions へ移行する
  2. Step Functions のマスキング、mysqldump 処理を並列化する

本稿では、本番環境のマスキング DB (クローン DB) の作成フローを Jenkins から Step Functions に移行した経緯をご紹介しました。

この記事がどなたかのお役に立てれば幸いです。

parallel_testsを使ってCIの実行時間を改善しました

こんにちは、シンクロ・フードの中川です。

今回は、弊社で運用している Rails アプリケーションにおける、CI での RSpec 並列実行のための parallel_tests 導入と、その結果について紹介します。

背景

弊社ではGitHub Actions を使って Rails アプリケーションの自動テストを実行しています。しかし、テストケースの増加に伴い、CI の実行時間が長くなり、開発サイクル全体の速度を低下させるという課題がありました。

そこで、RSpec を並列で実行できる parallel_tests という gem を導入し、CI の速度改善を行うことにしました。parallel_tests は CPU のコア数に応じて並列実行が可能であり、私たちが利用している GitHub Actions の Runner 環境 (2 コア) を最大限に活用でき、追加コスト無しで導入できるということで採用しました。

parallel_testsとは?

parallel_tests は、RSpec や Minitest などのテストを並列実行するための gem です。CPU のコア数に応じてテストを並列で実行することで、CI の実行時間を短縮できます。 github.com

導入方法

  • parallel_testsのインストール
    gem 'parallel_tests', group: [:development, :test]

  • database.ymlの設定
    parallel_tests の README に記載されている通り、環境変数 TEST_ENV_NUMBER をデータベース名の後に追加します。並列実行数に応じて、yourproject_test、yourproject_test2、yourproject_test3 のようにデータベース名を指定します。
    database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>

  • データベースの準備

    • データベースの作成
      parallel_tests では、並列実行する数だけ データベース を用意する必要があります。
      通常は parallel:setup を使えば データベース の作成とスキーマのロードができます。
      しかし、弊社では Ridgepole という gem をラップした独自の仕組みで データベーススキーマを管理しているため、parallel:setup が利用できませんでした。そこで、まず1つのデータベースに対して Ridgepole を用いたスキーマ適用を行い、その後 parallel:prepare を用いてコピーする方式を採用しました。
    • MySQL の権限設定
      もともと、RSpec を実行する Rails アプリケーションで使用する MySQL ユーザーに、データベースの準備のために必要なCREATE や DROP などの権限は付与していませんでした。そのため、parallel_tests を使用する開発環境と CI 環境に限定して、権限を追加しました。
    • 初期データの追加
      parallel_testsでは -e オプションを使うことで、複数用意されたDBに対して任意のタスクを実行できます。
      弊社では、初期データの投入に seed_fu を使用しているため、以下のコマンドで実行しました。
      RAILS_ENV=test parallel_test -e "rake db:seed_fu"
  • RSpecの並列実行
    parallel_rspecコマンドでrspecを並列で実行します。
    bundle exec parallel_rspec --test-options '-fd --force-color'

結果

CI実行時間の変化

アプリケーション 項目 Before After 改善率(%)
A DBのセットアップ 35s 83s -137%
rspec 9m51s 6m37s 33%
B DBのセットアップ 31s 57s -84%
rspec 3m39s 2m32s 31%
C DBのセットアップ 43s 109s -153%
rspec 24m58s 20m16s 19%

2 並列で実行した結果、RSpec のテスト時間を 20%~30% 程度短縮できました。
一方で、データベースのセットアップ部分ではアプリケーションによっては2倍以上遅くなってしまいました。

課題

  • テストケースの偏りの解消
    今回の検証では、アプリケーション C において、並列実行した RSpec の実行時間に差が見られました。これは、テストケースの実行時間の偏りが原因と考えられます。

    • 1 回目の計測: 17m4s と 19m27s
    • 2 回目の計測: 17m36s と 20m36s

    parallel_tests には、テストの実行時間を均等にする機能があり、それを利用することで、さらなる速度改善が期待できます。今回はログ取得の必要があったためこの機能は使用しませんでしたが、今後の課題として検討していきます。

  • データベースのセットアップの改善
    現在の方法では、データベースを直列で準備していることと、parallel:prepare がスキーマファイルをダンプ・ロードするため、処理に時間がかかっています。RSpec の実行時間と比較すると影響は小さいものの、改善の余地があるため、より効率的な方法を検討する必要があります。

おわりに

parallel_tests 導入により、追加コストをかけずに CI 実行時間を短縮し、開発効率向上に貢献できました。

しかし、改善の余地はまだあります。今後も、CI 環境を最適化し、より迅速な開発サイクルを実現していきたいと思います。

2025年3月時点のシンクロ・フードの技術構成

シンクロ・フードの越森です。

以前、2022年8月に技術構成を紹介する記事を書きましたが、2025年3月末時点での代表的な技術構成をお伝えする記事を書きたいと思います。 求人票にも書いたりしていますが、ここではより詳しく説明をしたいと思います.

言語・フレームワーク

  • Ruby & Ruby on Rails
  • Java & Seasar2
  • PHP & Lumen
  • Express.js
  • React, TypeScript

3年前から変わったこととしては、M&Aによりサービスを譲受したことで PHP & Lumen、Express.js のシステムが増えました。
PHP & Lumen のシステムについては現在 Rails にリプレイスをしているところになります。
また、Java のシステムについてもリプレイスが進んだことで日常の開発で Java のシステムを触る機会は少なくなってきています。
引き続きリプレイスは進めていきますが、社内の古いシステムなど一部の Java のシステムは残ってしまうため、今後も Java の開発をすることもあると思います。
Rails の自動テストのカバレッジ率は大分改善して、サービスによって差はあるものの 75〜95%くらいになっています。

フロントエンドに関しては、古いコードで jQuery は残っていますが、基本的に React で実装することが多いです。
3年前と比較すると React について社内勉強会を実施したり実装が増えたことで利用機会が増えています。
(SEO に影響するところでは Server Side Rendering をしていないこともあって利用していないのですが).

DB

  • Aurora MySQL v3(MySQL 8.0 互換)
  • Solr
  • OpenSearch
  • BigQuery

3年前は OpenSearch の導入を進めている段階でしたが、こちらの記事にあるように求人飲食店ドットコム、その次に飲食店ドットコム(記事)と導入しています。
他のシステムに関しては大きく変わっていませんが、BigQuery には Analytics や Search Console のデータを入れるようになってよりデータの活用を進めています。

開発環境

特に希望がなければ MacBook M4 Pro チップ、メモリ48GBのものを貸与します。 開発環境は Rails に関しては Docker で構築しています。 3年前の時は Docker が遅いということを書いていましたが、Docker の改善やコンテナの arm 化を進めたことで速度は大分改善しました。 Java については状況は前回から変わっていなくて、Eclipse であったり VSCode であったりとエンジニアの好みで自由に開発環境を作っています。

インフラ

  • AWS (ECS, EC2, Aurora, Lambda, S3, SQS...etc)
  • さくらクラウド
  • Nginx & unicorn
  • Apache & Tomcat
  • SendGrid & Postfix
  • Datadog & Nagios
  • Scutum
  • Heroku

インフラも3年前とほぼ変わりがなく AWS をメインで利用しています。
M&Aによるサービス譲受により Heroku の環境が増えています。
弊社では Heroku での運用ナレッジがないこともあり AWS に移行したくなってしまいます。

新しいインフラを構築する際は Terraform, Ansible を利用してコードとして記述して、コードレビューを実施した後に本番に適用するようにしています。
残念ながら未だにレガシーなインフラについてはコード管理できていないところがありますが、引き続きコード化を進めたいと思っています。

Rails は、ECS 上で Nginx, unicorn がコンテナで動作しています。新規のRails アプリケーションについては、Fargate を利用するようにしています。Java 系システムは EC2 上の CentOS にて Apache と Tomcat を動かして動作しています。
現在、Java 系システムのインフラの刷新を進めているところです。

メール配信は、 Rails 系システムに関しては SendGrid 、Java系システムは Postfix 、キャンペーンメールに関しては 自社で制作した Java アプリケーションと Postfix を組み合わせて送っています。SendGrid のキャンペーンメール機能で送らないのは、コストで折り合いがつかなかったためです。自作故に低コスト運用できている状態です。

監視は Nagios, Datadog, CloudWatch アラームを利用しています。

WAFについても引き続き Scutum を利用しています。 Scutumは、新しい攻撃に対しての対応も早く大変心強いです。

3年前と比較して大きな変化としては、こちらの記事でも紹介しましたが、社内に存在していたサービスに関わるインフラを全てクラウドに移行しました。
1年に1回の本社ビルの全館停電を恐れることがなくなって一安心です。
全館停電後にサーバーを起動させていくと、何かが壊れているということがそれなりの頻度で発生して対応が大変だったのですが、その恐怖を感じなくて良くなったのは大変嬉しいです。
(サーバーは UPS につなげた上で、停電前にシャットダウンするようにしているのですが何故壊れてしまうのだろうかと)

CI/CD

CI に関しては、Drone を廃止して GitHubActions に一本化しています。
CI では RSpec, Rubocop, Prettier, Querly などを動かし、linter 系の結果は reviewdog を利用して GitHub の PullRequest にコメントするようにしています。 CI の改善についてはチームを横断した CI チームを組織し、そのチームにて改善を進める運用となっています。

CD に関しては サービスによって方法は違ったりしますが、社内インフラからクラウドへの移行により、Jenkins でのデプロイから AWS Code サービス群に変わっています。

モバイルアプリ

Webアプリだけでなく、モバイルアプリも開発しています。
ネイティブ開発は iOS, Android ともに実施していて、それぞれ以下のようになっています。

iOS

Swift5 系、Xcode16 系 で開発しており、サポートバージョンは 現時点で iOS16以降。二世代前までサポートする方針です。
現在、Swift6 に上げるためにも、まずは Swift Concurrency の対応を進めています。
アーキテクチャは MVC、 View は Storyboard + Xib で作成しており、まだ SwiftUI は使っていません。
その他には色々なライブラリを使っていますが、それは割愛します。

Android

Kotlin で書いており、引き続き MVC から MVVM に移行中です。
なるべく Google や Jetpack が提供するものに寄せる形にリファクタリングしようとしており、Rx から Kotlin Coroutine, LiveData/Flow に変えていっています。
サポートバージョンは minSdkVersion が API レベル 26(Android 6.0)、targetSdkVersion が 34(Android 14)となっています。
(現在、targetSdkVersionは35の対応を進めています)
こちらもライブラリなどは割愛します。

コミュニケーション

バージョン管理は Git 、レビューは GitHubEnterprise、 チャットツールは Slack 、ドキュメンテーションは esa 、課題/タスク管理は Redmine / Notion です。
3年前と大きく変わったこととしては、フルリモート制度になりました。 そのためコミュニケーションについては、基本的にはテキストコミュニケーション中心ですが、口頭で話した方が早いことはさっと Slack のハドルでやり取りしてしまいます。 現在、会社ではコミュニケーション促進の制度として、3ヶ月に1回会社から飲み会の費用が出ることもあり、そのタイミングで出社するようになっています。


以上、大まかに技術構成をまとめてみました。

2022年と比べると改善しているところはありますが、大きくは変わっていない印象です。 今後も着実に一つ一つ改善を積み上げていきますので、開発を一緒にしてくれる方をお待ちしております。

Railsでの経験がなくても、Webアプリケーション開発の経験があれば大歓迎ですので、シンクロ・フードに興味があれば、気軽に連絡ください!

www.synchro-food.co.jp

生成AIを利用して物件登録にかかる時間を大幅削減した話

はじめまして。開発部の小関と髙木です。

今回は弊社のサービスである「飲食店ドットコム 店舗物件探し」の機能として、生成AIを活用した物件登録補助機能を提供することになったので、開発の進め方と実装した機能についてお話できればと思います。

飲食店ドットコム 店舗物件探し

まず、弊社のサービスである「飲食店ドットコム 店舗物件探し」について紹介いたします。本サービスは、飲食店が得意な物件探しサイトとなっており、店舗に強い不動産会社と提携することで、多数の店舗物件や居抜き物件を掲載しております。掲載されている物件には、最寄駅や賃料など物件情報が満載となっております。

機能開発の背景とプロジェクトの流れ

まず、機能開発に至る背景とプロジェクト進行の流れを大まかに触れておきます。

弊社では2023年から生成AIの利活用を実験的に始めており、運営するサービスの一部で生成AIを用いた新機能の開発やリリースを行い、徐々に開発ノウハウを溜めてきていました。

そのノウハウを他サービスにも横展開できないかと検討している中、弊社の「飲食店ドットコム 店舗物件探し」サービスにおいて、物件情報を登録する入力作業の手間を短縮させられないかという話が挙がっていました。 不動産会社の担当者は、基本的には不動産会社の店頭に掲載されているような物件の概要、間取り図、地図などをまとめた資料である「マイソク」と呼ばれる物件情報を見ながら手入力で登録をしており、その手入力に時間がかかっているため、入力を自動化できれば課題解決に繋がるのではないかという話です。

そして、その自動化には生成AIが有効に使える見込みがあったため、弊社GPTプロジェクトチームにて実現可能性の検証を行った結果、マイソクからテキストデータを抜き出して、その中から登録に必要な情報をChatGPTといった大規模言語モデル(LLM)に整理させることで、完全な入力の自動化は難しくとも、入力補助という形であれば実用的な精度で可能だとわかりました。

この検証を経て、設計、デザイン、そして開発へと進め、昨年夏にリリースに至ったというのが今回のプロジェクトとしての大きな流れでした。

今回の記事ではその中でも、GPTプロジェクトチームで進めた実現可能性検証のPoCとプロンプトチューニングについてや、サービス開発チームにおける実開発の中で、どういった課題や工夫があったかについて触れていきます。

※なお、本記事の検証等で使われたLLMのモデルは検証当時においての最新にすぎず、現在の最新モデルでは精度が大きく異なるため、ご留意ください。

PoCを通して確認したこと

今回、マイソクの自動入力ができないかという要望があった段階から、すでにある程度の機能の全体像は固まっていました。 それは、PDFまたは画像ファイルになっているマイソクデータから、OCR(光学文字認識)等の技術でテキスト情報を取得し、そこから登録に必要な情報だけを正確に抜き出すことができれば、登録はその情報を当てはめるだけで済むはずであるというものでした。

処理の流れのイメージ図

ただ、今回の前述の流れの中で特に壁となりそうだったのが、 "マイソクの形式は一定ではない" ということでした。 マイソクは不動産会社ごとに独自のフォーマットが存在し、マイソクごとの差としては次のようなものが挙げられます。

  • 間取り図が左側、物件情報の表が右側にあるマイソクもあれば、それらが上下の関係になっている全く別の構成のマイソクもある
  • 物件情報の表中の項目の並び順が異なる(賃料から始まるものもあれば、住所から始まるものもある)
  • 賃貸物件においては同じ意味である「保証金」と「償却」のように、マイソクによって異なる表現が複数存在する

このように、どのようなマイソクにも対応できる機能として作るには、OCRによるテキスト情報の抜き出しが仮にできたとしても、住所っぽい情報を住所であると判断したり、「賃料」といった項目名と「20万円」といった項目内容がバラバラであってもそれらが対応する情報であると判断する必要があり、そういった曖昧な情報の整理に対してLLMが活用できるのではないかという話になっていました。

OCR後の乱雑なテキストにおける分断された住所情報の例

よって、PoCでは、OCRの質とLLMによる情報整理の質の2点において、一定以上の精度を担保できるかを重点に調査しました。

1点目のOCRに関しては、利用可能なサービスの候補はいくつかあったものの、Googleの Cloud Vision API のOCR機能が日本語の識別精度も高く、金額や総合的な観点からも早いうちにそちらを利用することに決まりました。
なお、のちに検証〜実開発の間ぐらいでリリースされた、Open AIのGPT-4oのVision機能による画像認識なども試してみたものの、当時はまだLLM系で画像認識をさせつつ分析させることに関しては出力の質が悪く、OCRで得たテキストデータをLLMに整理させるやり方のほうが良い結果が得られたため、採用とはなりませんでした。

2点目のLLMの情報整理という部分においても、当時ある程度は精度が良く、コストパフォーマンスもよかったOpen AIのモデル「GPT-4 Turbo」を利用することで、プロンプトチューニングにさほど時間を割かない段階でも7割程度は正しい情報が得られたため、チューニングによって8割程度は見込めるであろうという検証結果となりました。

これらを踏まえて、自動登録は情報の正確性の担保が難しいことからも、人間が必ずチェックする「入力補完」という立ち位置であれば問題ないという判断になり、機能開発に進むこととなります。

プロンプトチューニングで得た気づき

PoCを通して機能開発の実現は可能だと判明したあとは、なるべく多くのマイソクのパターンに対応できるようなプロンプトチューニングを行いました。

プロンプトチューニングとは、より欲しい出力結果を高い精度で得るために、制約の追加や出力例を明記するなど、LLMに与える指示を変えていく作業のことで、今回は特定のマイソクにおいてだけ正確な情報が取れても意味がなく、なるべく多くのマイソクで情報が取れるよう、与える制約も汎用性を考慮しながらチューニングする必要がありました。

今回はマイソクのサンプルデータを複数用意し、人間が目検で抜き出した正解の情報を元に、すべてその情報と同じようにデータを取得できた場合に100%の精度とし、プロンプトで与える制約や指示の仕方を変えながら、多くのパターンで検証していくといったシンプルな方法を取りました。

そして、ある程度はチューニングによってPoCの段階よりは精度を高められたものの、単純なチューニングではまだまだ正しく抜き出せないデータもある状況でした。

具体例を挙げると、住所が2行に改行されているようなマイソクで、OCRの結果として得られるテキストとしては住所1行目と2行目が散らばってしまうものなどがありました(1行目が「東京都渋谷区恵比寿」で2行目に「1-7-8」とあるようなものだと、「... 東京都渋谷区恵比寿 賃料 1ヶ月あたり 1-7-8 ...」のように住所の間に別の文字列が混在してしまう)。
こういったデータは、マイソクによっては、2行目となってしまった番地以降の情報は住所情報の続きではないと誤認識して、1行目のみが住所として取れるといった事態が起こっていました。

プロンプトチューニングにおいては、こういった表記があればこの情報をこのプロパティとみなすといった指示の追加によって、ある程度の情報を抜き出せるようにはなっていたものの、前述のようにテキスト上で分断されてしまう情報は、本来はOCRのテキスト位置もデータとして扱わないと難しい状況ではありました(ただし、複雑性が増すため、今回は位置情報は使わずに進めていました)。

ただ、この課題は結論のみ言ってしまうと、チューニング期間中にOpen AIから「GPT-4o」という新モデルがリリースされ、それを利用するだけでほとんど解決されることなりました(結果的にGPT-4oを採択)。

これだけを聞くと、やはり、新モデルによる精度向上のインパクトが大きいわけですが、プロンプトチューニングをする中で有効に感じたものもいくつかありました。

その中でも特に、今回の場合においては「取得する要素の分解」が精度の向上に有用に働きました。
例えば、PoCの段階では「物件の所在地」は「住所」という単位で取っていたものの、実際の物件登録画面では「都道府県」「市区町村」「丁目」「番地」などが分かれており、APIのJSONレスポンスとして対応した項目ごとに返すためにも分解することになったのですが、それによって正しく取れるようになる物件情報がありました。
他にも、同じ賃料でもマイソクによって税抜き表記と税込み表記が異なったり、両方記載されているパターンがあることで、実行ごとに結果が変わることがあったため、税込みかどうかのフラグを分解することで解決した箇所もありました(※ランダム性に寄与するtemperatureは検証時から0で設定しています)。

逆に分解せずにまとめることが有効な場合もありますが、分解することでプロパティごとに細かく指示を出しやすくなった点も含めて、そういったチューニングも場合によっては有効となることが今回のチューニングを通しての気づきでした。

このように、モデルの精度向上とプロンプトチューニングによって、PoC時点よりも10~20%ほど正確に情報が取得できるようになり、そうしてできた最終的なプロンプトは開発担当に引き渡され、機能開発へと続きます。

物件登録補助機能の提供

続いて、物件登録補助機能の提供について説明します。
前述したように、今回実装する物件登録補助機能は、物件登録作業の中で一番時間を必要とする「物件詳細情報の入力」に焦点を当てています。要するに、アップロードされたマイソクから物件詳細情報を抽出し、フォームへ自動で入力してくれる仕様となっております。特筆すべき点として、入力作業自体をほとんどスキップできるように、登録に必須な項目に重きを置いて情報を抽出するようプロンプトが調整されています。結果として、物件登録補助機能を利用することで、以下のように実務のフローを変更することができました。

補助機能なしフロー
補助機能ありフロー

この仕様を満たすため、物件登録補助機能は図に記載の処理フローとして実装しました。

今回は外部のAPIを利用しており、すべての処理が完了するまでにある程度の時間が必要となってきます。そのため、非同期で外部のAPIを利用できるように、主要な処理をすべてSidekiqで行うように実装しました。また、非同期で処理した結果を自動入力する必要があるため、ポーリングの仕組みをクライアント側の技術スタックであるReactと、サーバー側の技術スタックであるRailsを利用して実装しています。
非同期処理は、マイソクの画像化、OCR、生成AIによる情報抽出といった流れとなっています。マイソクを画像化するにあたって、今回はImageMagickとGhostscriptを利用しました。PDFをJPEGに変換しているのですが、その際に透過部分の処理や複数ページの対応など考慮すべき点が多く、日頃必要とされない技術分野の知識が要求される難しい内容でした。OCRと生成AIによる情報抽出は外部のAPIを利用しているため、仕様書を読み込むだけではなく、発生しうるエラーをどのように扱うべきかを意識して取り組みました。

利用アンケートの結果

本機能の効果を確認するため、不動産会社宛に利用アンケートを回答いただいております。
ここでは、利用アンケートの結果を共有したいと思います。

物件登録補助機能ですが、リリース当初から多くの不動産会社に利用していただいております。さらに、物件登録補助機能をご利用していただくことで、半数以上の利用者が物件登録の際に3分以上短縮できていることがわかります。従来の物件登録は平均6分必要であることを考えると、物件登録補助機能が大幅な工数削減に貢献できていると捉えることができます。

利用意向
利用頻度
工数削減度合い

ただ、マイソクからの読み取り精度・速度の面では改善の余地があるといった内容でした。特に、読み取り速度は約半数が遅いと感じていることがわかりました。

読み取り精度
読み取り速度

物件登録補助機能の改善

前述した利用アンケートの結果から、読み取り精度・速度を向上するための後続対応を実施しております。具体的には、画像変換処理を削除したこと、物件情報の抽出ジョブの同時実行数を増やしたことが挙げられます。画像変換処理は基本的には1〜2秒程度で終了するのですが、PDFのページ数などに依存してしまうためボトルネックの一つとなっていました。そこで、PDFをJPEGへ変換するのではなく、利用するAPIを都度切り替えるようにすることで画像変換処理自体を廃止することができ、読み取り時間全体の平均値である10秒のうち、平均して2秒程度短縮することができました。また、物件情報の抽出ジョブの同時実行数を増やしたことで、処理が始まるまでの待機時間も削減させることができました。

まとめ

ノウハウの活用と不動産会社の需要に応えるため、生成AIを活用した物件登録補助機能を実装して提供することができました。PoCやプロンプトチューニングを通して、生成AIに対するノウハウをさらに高めることができたと考えています。また、実際に利用していただいた不動産会社からも、便利で物件登録の負担を減らすことができたといったお声をいただくこともできました。

弊社は今後もサービス全体を通して生成AIを活用した機能提供を継続してまいりますので、新たな知見やノウハウを積極的に発信していきます。

DevOpsの変更リードタイムの可視化システムを作成する

こんにちは、2024年新卒入社、開発部の岡塚です。現在は主に求人飲食店ドットコムなどの開発に携わっています。

弊社の新卒エンジニア研修ではWeb開発技術の基礎からシステムの設計・開発までがカリキュラムとして組み込まれています。今回はその中でもシステムの設計・開発を学ぶ「システム設計研修」で私が開発を担当したテーマであった「変更リードタイム可視化機能」について紹介します。弊社の新卒研修については、以下の記事で詳細に紹介されています。

tech.synchro-food.co.jp

背景

弊社では開発生産性向上活動の一環としてFour Keysを用いた生産性計測が行われています。生産性可視化の取り組みについては、過去の記事で詳細に紹介されているのでよろしければご覧ください。

tech.synchro-food.co.jp

上記記事の「まとめ」のセクションでも触れられていますがFour Keys計測は継続的・自動的に集計する仕組みが用意されておらず、集計作業に工数がかかっていることが課題となっていました。そのため今年のシステム設計研修では、変更リードタイムの自動集計システムが題材の一つとして選ばれ、今回私がそれに取り組みました。
以下に作ったシステムの画像を貼ります。元々はシェルスクリプトとGoogleスプレッドシートを用いて集計を手作業で行なっていたものを、今回の開発でシステム化しています。

既存のスプレッドシート:

変更リードタイム
案件の対応経過日数

今回作成したシステム:

変更リードタイム
案件の対応経過日数

設計方針

今回のシステムを開発するにあたり、以下の方針で設計・実装を行いました。
Ruby on Railsでバックエンドと、フロントエンドのグラフ以外の部分を実装しています。

  1. 内製の案件管理システムにweb APIの追加
  2. 変更リードタイムの計算に必要となるFirst Commitを取得する際にGitHub GraphQL APIを使用するため認証にあたってGitHub Appを使用し、GitHubと内製の案件管理システムからデータ取得を行うバッチの実装
  3. Chart.jsを利用してフロントエンド側のグラフ部分を作成

これは以下の様な制約があったことから上記の方針を採用しました。 

  • 新卒研修で作成したシステムの運用や保守にはあまり労力を割けない観点から、既存のインフラ上で構築を行いたい
  • 新卒研修の性質上、一般的なweb開発としてアプリケーションのレベルでの実装を行いたい

以下でデータ取得についてシーケンス図で簡単に記載した上で、各項目について説明していきます。

データ取得について

変更リードタイムを取得するための情報源が案件管理システムとGitHubであるため、両方から情報の取得を行なっています。

そして、

  • 案件起票日や担当者などのデータは案件管理システムから取得する必要がある
  • 一方で案件のfirst commitのみGitHubから取得する必要がある

という理由のため、GitHub GraphQL APIにアクセスしつつ、案件管理システムのテーブルのカラムの一部分の同期をする動作を行う形になっています。

処理方式について定期的に差分同期(一定の時間の範囲内で作成・更新されたデータに限定した同期)を行っており、初回実行時のみ全量同期(データ全体の同期)を行なっているのですが、この理由については後述します。

web APIの追加

同期処理によるデータ取得を行うため、案件管理システム側にweb APIを作成しました。(単純なweb APIのため、詳細は割愛します)

GitHub Appを使用した認証・データ取得処理の実装

変更リードタイムシステムは要件管理システム用のdbとは別のインフラで運用されることになるため、新たに変更リードタイム集計のためのテーブルを作成し、案件管理システムとGitHubから取得した情報をこのテーブルで管理するようにしました。

GitHub GraphQL API経由でfirst commitの日付を取得する過程でGitHubの認証が必要になるため、認証方法を選択する必要がありました。今回はGitHub Appを使用しています。各種選択肢がある中で一般的に個人利用ならPersonal access token(PAT)がその手軽さから使われることが多いと思うのですが、

  1. Personal access tokenの場合認証情報が個人に紐づく形になる
  2. 期間制限なしでPersonal access tokenを利用することが非推奨とされている

という懸念点がありました。そのため長期運用の観点から、懸念点の両方に対応できるGitHub Appを使用して認証を行う形にしました。
GitHubの認証をRubyで行う方法としては、専用のライブラリの導入なしで利用する形を取りました。OctokitというGitHub公式のapiクライアントを使うのが一般的ですがそれを使わなくても認証自体は可能で、また今回はライブラリを導入したときの使用箇所がこの機能に限定されるためです。以下の様な実装で認証を行うことができます。

private

      # Faraday経由でGitHubへのコネクションを取得
      #
      # @param path [String]
      # @param options [Hash]
      # @return [Faraday::Connection]
      def faraday_connection_github(path, options = {})
        jwt = generate_jwt(
          Settings.app_id,
          Settings.private_key,
        )
        access_token = access_token(Settings.installation_id, jwt)

        Faraday.new(
          url: github_url(path, options),
          headers: { 'Authorization' => "bearer #{access_token}" },
        )
      end

      # app_idとprivate_keyからJWTを生成する
      #
      # @param app_id [Integer]
      # @param private_key [String]
      # @return [String]
      def generate_jwt(app_id, private_key)
        private_key = OpenSSL::PKey::RSA.new(private_key)
        payload = {
          iat: Time.zone.now.to_i, # 発行時間
          exp: Time.zone.now.to_i + dummy_duration, # 有効期限
          iss: app_id, # GitHub Appのapp_id
        }
        JWT.encode(payload, private_key, 'RS256')
      end

      # apiからGitHubのアクセストークンを取得する
      #
      # @param installation_id [Integer]
      # @param jwt [String]
      # @return [String]
      def access_token(installation_id, jwt)
        url = github_url("/api/v3/app/installations/#{installation_id}/access_tokens")

        response = Faraday.post(url) do |req|
          req.headers['Authorization'] = "Bearer #{jwt}"
          req.headers['Accept'] = 'application/vnd.github.v3+json'
        end

        JSON.parse(response.body)['token']
      end
  end

上記の認証用の実装をした上で、案件管理システム、GitHubからのデータの同期用のメソッドをクラスメソッドとしてそれぞれモデルに追加しておきます。

データの同期というものを単純に考えた場合、データ全体の同期を定期的に実行しよう、という考えがありうると思います。ただしその場合、時間が経つにつれてデータ量が少しずつ増えていくため、次第にシステムにかかる負荷が青天井に高まってしまうという懸念がありました。そのため、

  • システムの稼働開始初回のみデータ全体の同期を時間をかけて実行する
    • 具体的には、クラスメソッドを呼び出すスクリプトを用意してFargate上で実行する
  • 以降は定期的に差分同期を行うことで実行時間を短く抑えられるようにする
    • 具体的には、同期の成功・不成功を管理するためのログテーブルを用意した上で前回の成功日時〜現在、までの分の差分同期を行うスクリプトをSidekiqのworkerとして作成し、定時実行する

という形をとっています。

Chart.jsをメインに利用してフロントエンド側の画面を作成

フロントエンド側のグラフはChart.jsを用いて実装しました。
大枠の仕組みは既存の機能で実装できたものの、冒頭の画像で示したヒストグラムのパーセンタイルの垂直線の表示の実装が難しかった点でした。

パーセンタイルの垂直線をヒストグラムの各階級幅内に表示するような機能をしたかったものの、そのロジックがChart.js本体や既存の公開されているライブラリではうまく実現できない状態でしたが、Chart.jsのインターフェースを利用することでプラグインを自作することが可能だったため、それを用いることで実装を行いました。
具体的には以下のようにafterDrawに渡したコールバックで、グラフ本体の描画完了時にアノテーション用のロジックを呼び出しています。

const plugin = {
    id: 'percentile-annotation',
    afterDraw: (chart: Chart) => {
      // ループを回す
      for (const [percentileKey, percentileValue] of Object.entries(
        percentiles,
      )) {/* 手続き的にパーセンタイルの垂直線を描画する */}
    },
}
const leadTimesChart = new Chart(ctx, {
    plugins: [plugin],
    data: {
      // 省略
    },
    options: {
      // 省略
    },
})

学んだこと

まず、完成させることができてよかったです!
今回の研修では、学び・反省になったことが何点かありました。

仕様を理想の目標からの逆算で決めるべきだった

今回のシステム設計研修にあたってはMUSTで求められている仕様というものが少なくWANTの仕様が多かったため、開発を進めながらどこまでやるかを決めて、研修終了までの時間を考慮してWANTに随時対応する、という形で進めることを計画していました。
実際、研修終了までに時間の余裕があると判断し追加でWANTの機能を開発していったのですが、実は仕様面での考慮不足があった、ということが複数発生して期限に間に合わなさそうになってしまっていました。最初の仕様を固める段階で、実際に実装に必要となる工数は考えずまず理想的に何を作りたいのかを依頼側とすり合わせる、その後に現実的な実現の可能性を考慮して難しい部分を削っていくという形の方が良かったかな、と思いました。つまり、まずは時間ファーストでなく理想ファーストで設計を行ってその次に時間などの現実的な制約を考える方が計画の立て方として筋が良かったかなと思いました。

仕様変更についてのレビュワーとの共有

仕様面での考慮不足による変更が発生したタイミングで変更内容をレビュワーに伝えきれておらず、手戻りが発生する場面がありました。仕様調整のやり取りを一元管理することでレビュワーから参照できるようにしたり、こちらから積極的にコミュニケーションして情報の共有を行っていく、などを行う必要があったと反省しました。

エッジケース・異常系に対する考慮

  • リードタイムが数ヶ月以上のスパンにまで長くなる案件が発生する
  • パーセンタイル計算のアルゴリズムの性質上、実際のリードタイムとしては存在しない数値が指し示されることがある
  • ネットワークエラーは何が返されるか不明なため任意のエラーをキャッチするべきだが、特定の型のみを捕捉していてエラーハンドリングが適切にできていなかったことに後から気づいた

といったエッジケースなどへの配慮が抜けていた部分がありました。
学生のときに趣味の一環で開発を行っていた時にはほとんど意識することがなかった部分だったため、このようなところへの配慮が必要だということはかなり学びになりました...

専門用語(ドメインの用語)についてのすり合わせ

リードタイムの定義については休日をどう扱うかなどいくつか考慮すべき点があり、設計の時点で細かく詰めきれていなかったためにレビュアーからの指摘で考慮漏れに気づくということがありました。また、パーセンタイルについては統計学の用語で必ずしも明確な定義があるものではなく(例: 取ることのできる補完のアルゴリズムが複数あり、それ次第で同じデータに対してでも数値が変わってくる)、既存のリードタイム計測の仕組みとの数値のずれが後になってから判明して修正するということがありました。このように専門的な用語・ドメインに関係した用語についてはおざなりにせずに、しっかりと定義を把握しておくことが大切だなと痛感しました。

まとめ

今回、研修テーマとして弊社開発部の生産性可視化システムを開発しました。生産性可視化の工数削減に貢献できていたら幸いです。
Railsを含むweb開発の最低限基本的な部分を設計から実装まで一通り触れて、かなり学びになったと思っています!今後、得た経験を活かして頑張っていきたいです。