NLP2023 に参加しました:発表編

こんにちは!技術部機械学習グループの山口(@altescy)です。

先日、沖縄にて開催された言語処理学会第29回年次大会(NLP2023)に参加してきました。 今年の大会は過去最多の参加者数となり、かつ久しぶりの本格的なオフライン開催ということで大変活気のある大会になったかと思います。 クックパッドからもML/NLPエンジニア3名が参加し、研究発表を行いました。

NLP2023会場裏の砂浜
NLP2023会場裏の砂浜

今週のTechLifeでは、今回から発表編、聴講編、座長編の3回に渡ってNLP2023の参加報告を各参加メンバーからお伝えしたいと思います。 ぜひお楽しみください!

クックパッドと自然言語処理

NLP2023での様子をお伝えする前に、クックパッドにおける自然言語処理技術・研究に関する取り組みについて紹介させてください。

クックパッドで主に扱われるレシピデータはその情報の大部分が自然言語で記述されるため、サービスや社内において自然言語処理が活躍する場面が多くあります。 レシピのカテゴリ分類レシピからの固有表現抽出レシピ入力補助のための食材提案レシピの分量予測など、レシピを対象にしたものだけでも非常にユニークなタスクで溢れています。 これまでにも上記のような業務における課題解決の中で生まれた成果を研究としてまとめて言語処理学会をはじめとした大会などで発表してきました。

例えば直近2年の言語処理学会年次大会 (NLP2021, NLP2022) では以下のような研究発表を行っています。

その他にもクックパッドでは多数の研究活動を行っていて、過去に発表した内容は research.cookpad.com から見ることができます。 今回もこうした研究活動の一環として NLP2023 に参加してきました。

NLP2023での発表の紹介

今大会では『レシピに含まれる不使用な材料等に関する記述の抽出』という題で研究成果の発表を行いました。 この研究では題の通りレシピ中のテキストに含まれる、不使用であることが明記された材料・調理器具・調理工程の記述を抽出するタスクに取り組んでいます。

不使用に着目する大きなモチベーションのひとつとしてあげられるのはレシピ検索における課題です。 通常の転置インデックスを用いた検索システムでレシピを検索する場合、「卵」で検索すると「卵なし」と記載されたレシピが検索結果に載ることがあります。 多くの場合、「卵」でレシピを検索するユーザーは卵を使ったレシピを探している可能性が高いと考えらるため、「卵なし」のレシピが登場すると違和感を感じるかもしれません。 そこで、レシピ中のテキストにおいて使わないアイテムをあらかじめ検出しておけば検索体験の改善に繋げられるのではと考え、不使用に関する記述を抽出するモデルの開発に取り掛かりました。

また、この研究はインターンシップに参加していただいた染谷大河さんと共に進めました。 染谷さんには特に後述のデータ拡張の検討や手法の比較実験に注力していただき、有意義な結果を残すことができました。 クックパッドでは自然言語処理や機械学習を扱う就業型インターンシップを募集していますので、ご興味のある方はぜひご応募ください!

Cookpad 採用サイト - [就業型インターンシップ] 機械学習コース

データセットの準備

モデルの学習・評価に用いるデータセットを作成するために、社内のアノテーターに依頼して1万件のレシピのタイトルと説明文に含まれる不使用な材料・調理器具・調理工程のスパンのアノテーションを行いました。 下図のようにテキストに対して不使用の対象となる領域とそのカテゴリを付与しています。

アノテーションの例
アノテーションの例

作成したデータセットの統計は以下のようになりました。 不使用の記述を含むデータ数(# texts w/ spans)はそうでないものと比べて少ないことがわかります。 そこで、より多くの正例を学習に利用できるように不使用の記述に着目したデータ拡張を試すことにしました。

データセットの統計
データセットの統計

今回は以下3つのデータ拡張手法を検討しています。 より詳細な設定は論文をご確認ください。

検討したデータ拡張手法
検討したデータ拡張手法

不使用抽出モデルの仕組み

今回作成したモデルは不使用と書かれたアイテムの領域を予測するスパン抽出モデルと、抽出したスパンのフィルタリングを行うモデルの2つから構成されます。

不使用検出モデルの構成
不使用検出モデルの構成

まず、スパン抽出モデルは BiGRU-CRF を用いた系列ラベリングにより不使用の記述とそのカテゴリを予測します。 BiGRU-CRF の入力は文字レベルで分割されたトークンです。 単語の境界の情報を与えるために、形態素解析により得られた品詞の情報を BIOUL スキーマでエンコードした系列を文字列と合わせて入力しています。 出力は材料・調理器具・調理工程のそれぞれのスパンに対応する BIOUL タグ系列となります。

入力テキストからスパンを抽出した後はフィルタ処理を行います。 ここではレシピの材料欄の内容を元に誤って抽出されたと考えられるスパンを除去します。 抽出したスパンのテキストと材料欄に書かれた材料名をそれぞれ正規化し1、もし材料欄の内容と一致するスパンがあればそれらを取り除く、という仕組みです。 スパン抽出モデルが抽出したスパンの中から実際にレシピ中で使われていそうな材料を取り除くことで Precision の改善が期待できます。

実験結果

提案手法を使ってテストデータに対して予測を行った結果が以下の表になります。 ベースラインとして、辞書ベースで抽出を行う手法 (Dictionary-based Extractor) と依存構造解析の結果を使う手法 (Dependency-based Extractor) を用意しました。

テストデータに対する予測精度
テストデータに対する予測精度

2つのベースライン手法に比べて、提案手法は F1 スコアで +20% 以上高い精度を達成しています。 データ拡張も Precision の向上に貢献し、全ての拡張手法を適用した場合に F1 スコアにおいて +2.1% の向上が見られました。 また、材料欄に基づくスパンのフィルタ処理も Precision の改善 (+2.3%) が確認でき、F1 スコアでも +1% の改善となりました。

誤りの例を見てみると「〜の代わりに」や「〜を避けたい」のように「なし」や「不使用」などに比べてレアな表現であることがわかりました。 また、カテゴリごとの結果を見てみると材料や調理器具は F1 86% 程度であるのに対し、調理工程は F1 77% 程度と他に比べて悪い結果でした。 材料や調理器具は一般名詞である場合が多いのに対して調理工程は「一晩水に浸け」や「粉をふるう」のように複雑な表現が存在するため、それが精度低下の要因になった可能性があります。 不使用抽出タスクにおいては、こうした多様な表現にいかに対応するかが今後の課題となりそうです2

ちなみに、最近話題の ChatGPT はこのタスクを上手く扱うことができるのでしょうか? 以下の画像は zero-shot / few-shot の設定で GPT-4 に不使用検出タスクを解いてもらった結果になります。

GPT-4による不使用検出の結果
GPT-4による不使用検出の結果

抽出したスパンとラベルの関係は正しく対応していますが、「さつまいも」や「ボウル」のように使用するアイテムも抽出されてしまいました。 もし人間がこのプロンプトを見たのであれば、このように間違うことは少ない気がします。 もちろん、プロンプトをもっとチューニングしたり、Chain-of-thought のようなテクニックと組み合わせることで改善する可能性はありますが、期待した結果を得るにはそれなりの工夫が必要そうです。 NLP2023でもLLMが否定表現を上手く扱えない可能性を示す研究があったように、「不使用」も LLM にとって苦手な表現なのかもしれません。

おわりに

NLP2023で発表したレシピから不使用に関する記述を抽出する研究について紹介しました。 今後は抽出した結果を使って実際にレシピ検索の改善などに応用できるか確かめていく予定です。

次回は聴講編として深澤(@fukkaa1225)が NLP2023 で発表された研究から特に興味深かったものをピックアップして紹介します!

ちなみに、クックパッドでは現在就業型のサマーインターンも募集中です。 ご興味のある方はぜひご応募ください!

cookpad.wd3.myworkdayjobs.com


  1. 材料名の正規化に関する記事はこちら: https://techlife.cookpad.com/entry/2017/10/30/080102
  2. RoBERTa などの事前学習済みモデルを使う方法も試しましたが、F1における精度向上は +1% ほどと限られたものでした。

モブプログラミングを1年以上継続するコツ

こんにちは、メディアプロダクト開発部のマーケティングサービス開発グループ(通称msdev)の id:asonas です。msdevウィーク最後の記事です。チームメンバーの記事も是非読んでみてください。

マーケティングサービス開発グループでは毎週月曜日13時から17時の決まった時間にモブプログラミングを実践しています。 このモブプログラミングの枠は1年以上継続していて、毎週様々な課題の解決や機能の開発をしています。この記事ではモブプログラミングを長く継続するためのコツをお伝えします。

モブプログラミングとは

まず、モブプログラミングとは、チームメンバーが同時にコーディングを行う手法です。重要な点はこの「チームメンバー」にはソフトウェアエンジニアだけではなく、プロダクトオーナーやデザイナーのような方々も含まれていることです。スクラムガイド(2020)の開発者と同じように考えてもらうと自然かもしれません。

モブプログラミングにはいくつかのメリットがあります。第一にチーム全員が共通の目標に向かって協力するためコミュニケーションの質が向上します。また、プロダクトに深く関わることで、知識の共有が促進され、チーム全体でより高い品質のコードを書くことができます。

さらに、モブプログラミングは開発プロセスを迅速化できます。複数の開発者が同時に機能を実装することで、エラーやミスがすばやく発見され、修正できます。これにより、開発プロセスがスムーズに進み、品質の高いコードをより迅速に提供できます。また、機能の実装途中に意見の分かれるポイントが出てきたときには、プロダクトオーナーやディレクター、デザイナーの方々の意見も取り入れることで"戻し"の作業を省くことができます。

モブプログラミングはチーム全体の知識や技術レベルを向上させることができます。開発者が一緒に開発をすることで、新しいアイデアやテクニックを学び、開発者自身のスキルを向上させることができます。これによりチーム全体の技術力が向上しより高度なプロジェクトに取り組むことで、いわゆる暗黙知から形式知、形式知から暗黙知へのループがモブプログラミングを通して回せます。

このようなモブプログラミングを2021年12月から16ヶ月以上実践しています。また、私たちのモブプログラミングは社員だけではなく、株式会社えにしテックさんの darashi さんと cafedomancer さんを招聘して毎週実践しています。

1年以上開催するうえでどのようなコツが必要でしょうか?

darashiさんとcafedomancerさんは遠方からZoomを使ってモブプログラミングに参加してくださっています。よくある定義として「ペアプログラミング・モブプログラミングはひとつのコンピューターでやる」とありますが、昨今の開発体験の進化により、Zoomによる画面共有や VSCode の Live Share などのツールの発展により遠隔地にいてもモブプログラミングは充分に満足できる形で開催できます。一昔前だと開発者全員でひとつの開発サーバーにsshで入ってscreenやtmuxのようなターミナルマルチプレクサのセッションを共有してコードを書いていましたね(なつかしい。10年以上前の話ですが)

定期的にモブプログラミングを実践するいくつかの良い方法を紹介します。 ひとつは毎週決まった曜日と時間に開催することです。

私たちのやっているモブプログラミングは毎週月曜の13時から17時に固定して実施しています(年末年始の休暇、祝日などが無い限りは基本的に開催です)。

これは私たちがスクラムを実践している点も関係しており、スクラムガイド(2020)には、

スクラムイベント スプリントは他のすべてのイベントの⼊れ物である。(略) スクラムにおけるイベントは、規則性を⽣み、スクラムで定義されて いない会議の必要性を最⼩限に抑えるために⽤いられる

とあります。私の中ではモブプログラミングもスクラムのイベントのひとつ*1 として取り組んだほうがお得だと思っていることです。

また、モブプログラミングは13時から17時の間で実施されるのでその朝会で取り組むことを決めています。モブプログラミングで取り組みたいことは日々のスクラムイベントから適宜Issueに起票されてラベルで管理されています。

ふたつ目は毎週のログを取ることです。

開催時に必ず全員で見るGoogleドキュメントがあります。僕たちのチームでは「モブプロメモ」と呼ばれています。

モブプロメモの様子

朝会で取り組むことが決まれば事前にこのモブプロメモへ書いておきます。13時になりZoomへ人々が集まり、モブプログラミングで取り組みたいことをオーナーがメンバーに説明をして開始となります。 基本的には、このメモには会話した内容のメモやコードを書きながら発生した議論をまとめたりしてあとから読み直せるようにしています(全文をメモするようなことはしません)。 モブプロは毎週開催しておりその成果物はPull Requestになりますが、休暇などで参加できない方に向けてもこのメモは役に立ちます。

モブプログラミングの座組

誰から取り組むか、という点については毎週の参加者でランダムに順番をきめています。

その日の参加者の名前を書きつつshuffleするスニペット

持ち込んだ課題のオーナーからはじめても良いのかもしれませんが、私たちは特に気にすることなく、ランダムに順番を決めています。明確な理由付けはないですが、施策や課題の知識の偏りがチーム内にあります。この偏りをうまく利用する形で施策の目的や達成したいことはモブプログラミングを通して共有できるようになっています。

そこで私が大切にしていることのひとつとして、自分がドライバーの時は頭の中で思っていることをすべてしゃべるようにしています。「このコードの意図は何だろう」「なるほど、ブランドごとにトピックスの最新の1件を取ってきて、その順序でブランドの一覧を掲出しているのか」「フロントエンドのテストで行数を指定して実行方法はどうやるんだっけ」「ここのコードの修正はvimでやるほうが得意なのでvimに切替えますね」というようなことをしゃべるようにしています。これはドライバーが思っていることをすべて言うことでモブの方々がドライバーの思考を理解しやすくするためです。逐一しゃべりながらやるので少し大変なのですが、ライブコーディングのような感覚でやると楽しめます。エディタやツールのテクニックも実況しながらやると「それなに?」のような会話も発生します。時には取り組む課題とは関係のない話題についても触れて開発体験の共有をするのもよいなと実感しています。

ペアプログラミングでもそうですが、どんどん交代しながら課題を解決していくので、25分+5分休憩を1セットとして回していきます。2時間ほど経過するタイミングで休憩の時間を15分取っています。

5,6人で回していくとなんだかんだで3時間30分ほど経過するので、最後の30分の枠でふりかえりをします。 ふりかえりではよかったこと、取り組んだ課題の難しかったところ、白熱した議論についてなど様々なことが書かれています。

ある日の振り返りの様子。この日は業務分析をしたりReactのテストに苦戦する様子が描かれてる

私たちが実践するモブプログラミングのまとめ

カタはだいたいこのような流れです。ただ、このカタにハマらずブレることもままあります。前週に性能改善系の課題をこなしたときには冒頭で性能改善の様子をメトリクスを眺めたりもしますし、新しい参加者がいれば自己紹介をしたりしますし、Ruby/Rails、ReactなどだけではなくSQLを眺めてみんなでウンウン唸りながら改善をすることもありますしコードをほぼ書かずに業務分析をすることもあります。 おすすめのツールや設定、スニペットがあれば自慢してみたり、最近の技術的な話題で盛り上がることもあります。

モブプログラミングはとてもハードなプラクティスです。一日4時間もやるととてもヘトヘトになります。それでもチームのモチベーションを維持のためにも緩急をつけて、たまには雑談を挟むことで機械的な開催になることを避けるように心がけています。仕事として楽しめるような雰囲気作りもとても重要です。

モブプログラミングを実践するうえで、特にはじめて参加されるチームメンバーの場合はアプリケーションに精通していないこともあります。暗黙知が備わっていない、わからないことは当然あります。ドライバーになる人であれば分からない旨を伝えることも大事ですし、モブの方々も率先して自分たちが持つ知見を展開する心意気も重要です。ここのサイクルを回していくことでチームの生産性が向上しより早くユーザーに価値を届けることができます。

ただ、ここまで書いた内容は、一朝一夕でなしえたものではありません。毎週のふりかえり以外にもチームのモブプログラミングの意義を問うようなふりかえりも別途実施しました。チームメンバー各位がモブプログラミングに対する認識を揃えるなどを経て今のモブプログラミングのカタが完成しています。

モブプログラミングを継続してやっていくうえでメンバーの練度の差も次第に揃ってきます。特に導入時などはバタバタとしてしまうこともありました。それでも毎週のふりかえりの積み重ねでよりよい体験へと持っていくことができます。

モブプログラミングを長く続けることで対外的な登壇もしたりしました。前日の pndcatさんがKaigi on Railsで登壇するきっかけになったプロポーザルも実はモブプログラミングで取り組んだ成果でした。

kaigionrails.org

今は違うチームに異動してしまったのですが osyoyuさんもモブプログラミングで取り組んだことが採択されました。

kaigionrails.org

今回の記事では私たちのチームでうまく、そして継続的にモブプログラミングを実践する方法を紹介しました。定期的に開催しつつも、義務的にはならずソフトウェアエンジニアとして楽しく取り組めるような雰囲気作りを紹介しました。 もし私たちの取り組みに興味がありましたら、以下のリンク、またはTwitterなどでDMを頂ければカジュアルな面談からでも実施できればと思います。

cookpad.careers

*1:正確にはモブプログラミングはスクラムイベントの枠組みではありませんし、スクラムガイドには定義されていません。どちらかというともっと大きな枠組みの文脈で語られることが多いです。が、そこはスクラムやモブプロもXPのかけらということで解釈してもいいよなと考えています。

ポリモーフィック関連を活用し、森羅万象の「いいね」を実現する手法

こんにちは!メディアプロダクト開発部マーケティングサービス開発グループ (msdev) のなどやま (@pndcat) です。業務では、クックパッドの広告の開発・運用や、新規サービスの開発をしています。本業の推し活動では、今年の夏はたくさんのイベントに参加するため、推し活動もがんばっていく予定です。

今週は msdev week として Techlife を更新しており、この記事は2日目になります。
1日目は三條さんによる「クックパッドの toB 向け事業における ChatGPT API の活用事例紹介」の投稿でした。
本記事も、メーカーズタウンに関するブログなので、ぜひこちらの記事もご覧ください!

はじめに

この記事では、Kaigi on Rails 2022 森羅万象に「いいね」するためのデータ構造 というタイトルで発表をした、Rails を用いたデータ構造のリファクタリングについて紹介します。ポリモーフィック関連を用いたリファクタリングにより、テーブルやコードの重複を排除し、メンテナンス性や拡張性を向上させることができました。データ構造をリファクタリングする背景や、具体的な手法、そしてなぜその変更を行うことができたのかについてを詳しく説明します。

目次

  • データ構造の初期設計
    • 新機能の「いいね」を実装したい
    • 最初のデータ構造
    • リリースをして1年... どうなったか...
  • ポリモーフィック関連を使ったデータ構造に変更
    • 新しいデータ構造: likes と anonymous_likes
    • リファクタリングの手順
      1. 新旧いいねのテーブルに書き込む (Write)
      2. 旧いいねを新いいねにマイグレーションする (バッチ)
      3. 新いいねを使う (Read)
      4. 旧いいねのモデルとテーブルを削除する (Delete)
  • まとめ

データ構造の初期設計

新機能の「いいね」を実装したい

msdev では、中小の食関連メーカーとユーザーをつなぐコミュニケーションプラットフォーム「メーカーズタウン」を開発しています。メーカーズタウンでは、メーカーはトピックスの投稿や商品の登録を行い、ユーザーはコメントやクチコミを投稿したり、いいねをすることができます。

リリースのタイミングで、「いいね」機能を導入することになりました。要件は、以下の3つでした。

  • 何に「いいね」ができるか? → トピックスと商品
  • 1つの対象に何回「いいね」ができるか? → 1回
  • 「誰が」いいねをすることができるか? → 誰でも (ログインユーザーと、未ログインユーザー)

特に、最後の要件をどう実現するのかについて悩みました。メーカーズタウンでは、ログインユーザーと未ログインユーザーのデータの扱いが異なるため、「いいね」の種類を分ける必要がありました。

ここまでをまとめると、「いいね」は以下の4種類に整理することができます。

  • ログインユーザーの商品のいいね
  • ログインユーザーのトピックスのいいね
  • 未ログインのユーザーの商品のいいね
  • 未ログインのユーザーのトピックスのいいね

最初のデータ構造

データ構造の案として、以下の3つの方法を検討しました。

  • 対象別とユーザー区分別で、4つのテーブルを作成する
  • ポリモーフィック関連
  • STI

結果として、対象別とユーザー区分別のテーブルを作成することに決めました。将来的に「いいね」の種類が増える可能性があったため、ポリモーフィック関連やSTIを用いたテーブル構造を選択することもできましたが、サービスが使用され続ける中で仕様が変わる可能性があるため、必要に応じてデータ構造を改善する方針としました。

リリースをして1年…どうなったか?

1年後、コメント機能やクチコミ機能が増え、「いいね」の対象が4つに増えました。これに伴い、テーブルが8個になったことで2つの問題が生じました。

  • 「いいね」の対象が増えるたびに、テーブル・モデル・コントローラーを毎回追加するのでつらい
  • 新しい対象について、いいね数の集計バッチを追加することを忘れ、集計漏れが発生した

テーブルを毎回追加することはできますが、新しいテーブルが追加されるたびに、集計バッチの修正を行うことを意識するのは難しいことです。

一方で、「いいね」の対象が4つまで増えたことで、「いいね」の仕様はすべて共通であり、今後も「いいね」対象別の振る舞いはなさそうということがわかりました。

ポリモーフィック関連を使ったデータ構造に変更

ポリモーフィック関連は、複数のオブジェクトを関連付ける場合に適しています。今回の場合は、商品、トピックス、コメント、クチコミに「いいね」をつける必要があるため、ポリモーフィック関連を用いて実現することができます *1

サービスを1年間運用した結果、商品、トピックス、コメント、クチコミの「いいね」に関する仕様や振る舞いが共通していることがわかり、ポリモーフィック関連に移行することを決定しました。移行に際して、チームで以下の2つの制約に合意しました。

  • 今後も「いいね」の仕様を変更しない
  • if 文を書くと破綻するため、if 文を絶対に書かない (=異なる振る舞いはさせない) *2

新しいデータ構造:likes と anonymous_likes

新しいデータ構造は、likes (いいね) と anonymous_likes (未ログインのユーザーのいいね) の2種類にまとめました。 ログインユーザーには UserID がありますが、未ログインユーザーには UserID はありません。そこで、初めのデータ構造と同じように、ログインユーザーと、未ログインユーザーは、2種類のテーブルに分けることにしました。anonymous_likes は、UserID の代わりに like_identifier というカラムを作りました *3

今までの product_likes テーブルは product_id, user_id の2カラムでしたが、ポリモーフィック関連を利用すると下図の likes テーブルの likable_id, likable_type, user_id の3カラムで表現します (他の *_likes テーブルも同様)。

では、モデルはどうなるかというと、今までは、4つのモデル (ProductLike、TopicLike、CommentLike、KuchikomiLike) があり、それぞれに対応するテーブルが存在していました。しかし、ポリモーフィック関連を利用することで、1つの Like モデルで表現できるようになりました。現在は対象が4つしかない「いいね」ですが、10個でも100個でも「いいね」の実装ができる、森羅万象の「いいね」のデータモデルが完成しました🌲🌳

※ before / after を見やすくするために、コードを画像にしています。

リファクタリングの手順

データの移行は、以下の手順で行っています。移行手順は特別な手法ではありませんが、Rails のポリモーフィック関連のマイグレーションの一例として、紹介します。

  1. 新旧の「いいね」テーブルに書き込む (Write)
  2. 旧いいねを新いいねにマイグレーション (バッチ)
  3. 新しい「いいね」を使う (Read)
  4. 旧いいねのモデルとテーブルを削除 (Delete)

1. 新旧のいいねテーブルに書き込む (Write)

これは、コントローラーの create メソッドで実行されます。上部分では、既存の ProductLike モデル (旧いいね) を作成し、下部分で Like モデル (新いいね) も作成します。1つのメソッド内で「いいね」がされた場合、新旧のいいねにそれぞれ書き込まれるようにします。

削除も同様に、旧いいねを削除しつつ、新いいねも削除します。ただし、この時点で必ずしも旧いいねと対応する新いいねが存在するわけではありません。Like が見つからないときのために、Safe Navigation Operator の & を destroy の前に書く必要があります。

2. 旧いいねを新いいねにマイグレーションする (バッチ)

次に、データのマイグレーションについて説明します。まず、product_likes テーブルのデータを likes テーブルに合わせたハッシュをつくります。
Rails 6からは、upsert_all メソッドが導入され、ハッシュをそのまま渡すことでデータを生成することができます。バッチを冪等に実行することができるため、今回のようなマイグレーションを行いたい場合は、upsert_all メソッドをぜひ活用してください。

新いいねと、旧いいねの ID (primary key) の順番がばらばらになることが気になるかもしれませんが、likes テーブルは、product_likes や topic_likes などの4つのテーブルを1つにマージするためのテーブルであり、ID の順序は意味を持ちません。重要なのは、「いつ」「誰が」「何に」に対して「いいね」を行ったのかというデータを正しくマイグレーションすることです。ID の順序については気にする必要はありませんが、「いつ」の情報をコピーするために、created_at を忘れないように注意してください。

また、「1. 新旧のいいねテーブルに書き込む」の段階では、旧いいねと対応する新いいねが必ずあるわけではなかったため、新いいねの削除に Safe Navigation Operator を付けていましたが、データ移行が完了したため、Safe Navigation Operator を外します。

3. 新いいねを使う (Read)

新いいねにデータが入ったので、アソシエーションを以下のように変更します。これからは、product.product_likes ではなく、product.likes を使います *4

「新しい対象について、いいね数の集計バッチを追加することを忘れる」という問題もありましたが、今回の変更により、1クエリで「いいね」を合算することができます。

さらに、「ある時刻以降の Like を求める」「あるユーザーの Like を求める」というケースのクエリも簡単に書くことができるようになりました。

4. 旧いいねのモデルとテーブルを削除 (Delete)

すべてのコードで新いいねを参照したら、旧いいねの書き込みを削除します。最後に、旧いいねのモデルとテーブルを削除することができたら、ポリモーフィック関連への移行が完了です 🎉

まとめ

本ブログでは、似ているけどちょっと違う「いいね」の設計の紹介をしました。最初は、変更に耐えられるようなデータ構造にし、実装が複雑になったタイミングで、データ構造を見直しました。今回は、ポリモーフィック関連へのリファクタリングをしました。

ポリモーフィック関連を適用できるケースは多くないと思いますが、場合によってはとても強力なデータ構造の手法であり、特に Rails ではフレームワークレベルでのポリモーフィック関連付けの支援があるため、テーブルやコード量を減らし、かつ、わかりやすいコードに置き換えることが可能です。
ポリモーフィック関連を利用したことがない人や、ポリモーフィック関連は SQL アンチパターンでよく挙げられているから抵抗があるという人も多いと思いますが、今回のブログを通して、今後のデータ構造でポリモーフィック関連を選択肢の一つに入れてもらえると嬉しいと思います。

クックパッドでは、toB 事業をやりたい!データ構造の話をたくさんしたい!リファクタリングに興味がある!というエンジニアを募集しています。以下のリンクからのご応募をお待ちしています!

cookpad.careers

*1:ポリモーフィック関連の具体的な実装に関しては、Railsガイド を参照してください

*2:Techlife: Kaigi on Rails 2022 にて『森羅万象に「いいね」するためのデータ構造』の発表をしました の「ポリモーフィック関連で if 文を書くと破綻するの例は?」 に詳細を書いています

*3:Techlife: Kaigi on Rails 2022 にて『森羅万象に「いいね」するためのデータ構造』の発表をしました の「匿名いいねの like_identifier ってなに?」を詳細を書いています

*4:grep をしやすいように、product.likes ではなく product.product_likes の書き方をしていました。product_likes は likes に書き換える必要があります