見出し画像

不適切なデータを生成してしまうRakeタスクを見直して修正した話

はじめに

こんにちは。ソフトウェアエンジニアの大橋(X: m_asa_o)です。

この記事では、Railsで動作確認用のデータセットを生成するためにONE CAREER PLUSの開発チームで採用している方法と、その改善事例をご紹介します。



従来の状態

転職サイト「ONE CAREER PLUS」では、開発用のダミーデータを生成するための仕組みとして、テーブルごとにデータ生成用のRakeタスクが存在しています。
例えば以下のように、求人に紐づくタグ(tags)のデータを生成する処理があります。(説明を簡略化するため、実際のコードを一部改変しています)

# lib/tasks/developments/dummy/tags.rake
task :create, ['size'] => :environment do
  tags = (1..size).map do
    { label: Faker::Lorem.sentence # Fakerでランダムデータを生成 }
  end

  RecruitFeatureTag.insert_all(tags) 
end

このRakeタスクによって作成されたダミーデータを使った例がこちらです。
(※過激な表現が含まれていたため、該当箇所にモザイク処理を施しています。)

問題1. データの中に不適切な表現が含まれる

Faker::Lorem.sentenceの生成処理に使われる辞書には、政治的に意味を持つ単語や、ネガティブな単語が一部含まれているため、単なる文字の羅列以上の意味を持った文字列が生成されることがあります。
該当データはすぐに削除しましたが、何かの拍子に誤って外部に出てしまった類の失敗談は例を挙げるときりがないため、このようなデータが生成される可能性があるリスクを見過ごすことはできません。

参照:

問題2. バグの混入に気づきにくい

Fakerで生成される長文は意味が通っていないため、一覧で大量にデータが並んだときに、意図したデータが正しい場所に表示されているかを見分けることができません。


Fakerの代わりに、自前でランダム文章を作る

これらを解消しつつ、かつ動作確認に適したデータを作るには、本番環境のデータをそのまま使えると良さそうです。しかし、これはユーザーの個人情報などもコピーしてしまうことになるため、セキュリティやプライバシーの観点で好ましくありません。
そこで、以下の手法をデータの種類に応じて使い分けました。

手法1: マスタデータをcsv化して、seedで登録する

そもそもデータの種類が限られており、日常的にデータの登録が頻繁に行われることがないようなテーブルについては、seedを使ってデータを準備することが適しています。
マスタデータを取り扱うテーブルについてはRakeタスクによるデータ生成を廃止し、全てseedで行うように変更しました。求人に紐づくタグもその一つです。

# db/seeds.rb
require 'csv'

CSV.foreach('db/seed_data/tags.csv', headers: :first_row) 
do |line|
   Tag.all.find_or_create_by(id: line[0], label: line[1]) 
end

これにより、以下のように固定データが入るようになりました。

手法2: 既存のRakeタスクに手を加えて、可能な限り本番データに近いデータが入るようにする

その他のテーブルでは、Fakerに代わって自前でテキスト生成を行うようにすることで、それらしいデータが入るようにしました。データを作成したい箇所で、以下のようにgenerate_dummy_textメソッドを呼び出します。

{
  career_change_reason: generate_dummy_text('career_change_reason', company.name),
  career_change_point: generate_dummy_text('career_change_point', company.name),
  useful_experience: generate_dummy_text('useful_experience', company.name),
}

メソッドの中身は以下のような単純な作りとしています。 先頭の企業名のみ可変として、それ以降に結合する文字列は、本番データを加工して用意しておいた定型文を使っています。

将来的には複数種類のテキストからランダムに選ばれるようにすることが望ましいですが、当面の回避策としてこのようにしています。

def generate_dummy_text(column_name, company_name)
  case column_name
  when 'career_change_reason'
    "#{company_name}でシステムの導入・運用の業務だけでは、顧客と伴走し課題解決のために柔軟に動く
自分の特性を最大限発揮できないと感じたことと、子供が生まれたことで年収を上げたいと思ったことが理由。"
  when 'career_change_point'
    "#{company_name}は強力な自社製品を持ち技術的なアドバンテージが他社と比べあることや、
セールスとエンジニアの垣根が高くないことを重視した。私はエンジニアではあるものの、顧客と話し課題を
一緒に見つけ解決していく行為が好きで、それが実現できる職種であることが大事だった。"
  when 'useful_experience'
    "#{company_name}では様々な会社のITインフラ構築にかかわり、
特にログ分析・収集を行うSIEMの導入や、ID管理アーキテクチャの設計、製品選定、社内ネットワーク構築など、
セキュリティ態勢整備の側面での案件を多く経験した。 プロジェクトマネージャー資格も取り、
社内外で100人ほどの規模の開発案件をリードしたこともあった。そういった前職での積み重ねが、
現職における顧客とのディスカッションでの引き出しの多さや、暗記した製品知識を伝えるだけではない、
自分で手を動かしてシミュレーションやモデリングを行い、顧客に提示する営業スタイルに繋がっている。"
  end
end

変更前後のデータを比較すると、その差は一目瞭然です。(黒塗りの箇所には企業名が入ります。)

変更前:


変更後:


さらに行ったこと

その他にも、Rakeタスクで生成されるデータを可能な限り本番データに近づけるため、例えば以下のような変更も行いました。

  • データ生成時に選ばれる企業を絞る

転職体験談のデータは開発環境で数千〜数万件生成しますが、企業の総数と比較すると非常に少ないです。そのため、企業をランダムで紐づけてしまうと、1企業あたりに紐づく転職体験談が極端に少なくなってしまいます。転職体験談データを数千万〜数億件作るようにすればこの問題は解消できますが、データの生成処理に莫大な時間がかかってしまいます。
そこで、データ生成時に選ばれる企業を本番環境でクチコミ件数が多い上位数百社に絞ることで、クチコミの分散を防ぎ、1つの企業に紐づく口コミが多くなるようにしています。
コードの設計面では、以下の工夫をしました。

  • Rakeタスクの中で処理が肥大化することを避けるため、データの生成を専用のクラスに切り出しました。

  • データ生成クラスの中では、FactoryBotを使ってレコードを作るようにしました。これは、データ生成タスクのメンテナンスがされなくなって壊れることを防ぐためです。開発者はテーブル定義に変更を加えた場合、テストを通すために必ずFactoryを修正します。そのため、データ生成タスクでもFactoryを使うことで、自動的に変更に追従させられるメリットがあります。

実際のコードはこちらです。(説明を簡略化するため、一部改変しています)

# lib/tasks/developments/dummy/career_changes.rake
task :create, ['size'] => :environment do |_task, args|
  Dummy::CareerChangesCreator.call(size:)
end
  
# app/services/dummy/career_changes_creator.rb
class Dummy::CareerChangesCreator
  attr_accessor :size

    def initialize(params)
    @size = params[:size] || 5000
  end

    def call
     companies = Company.having_many_reviews #=> クチコミ件数の多い企業に絞る
     user_ids = User.ids career_changes = size.times.map do
       company = companies.sample
       FactoryBot.build(
         :career_change,
          user_id: user_ids.sample,
          company_id: company.id,
          career_change_reason:
 generate_dummy_text('career_change_reason',company.name),
         career_change_point:
 generate_dummy_text('career_change_point',company.name),
         useful_experience:
 generate_dummy_text('useful_experience', company.name),
       )     end
     CareerChange.import! career_changes
   end
 end


今回使わなかった手法

今回は既存のRakeタスクに手を加える方針としたため、結果的には採用しませんでしたが、ほかにも動作確認用のデータを生成する方法として以下を検討しました。

1. 本番DBの一部をマスクしたdumpを開発環境に復元する仕組みを作る

この方法のメリットとデメリットとしては以下が挙げられます。

  • メリット:

    • 限りなく本番データと似通った条件で動作確認ができる

    • StagingやIntegrationで定期実行すれば、動作確認しやすい状態が常に保たれる

  • デメリット:

    • マスキングの仕組みを作るのが大変

    • 本番データが少ないテーブルでは、十分な量およびパターンの検証用データが生成できない

2. seedに一本化する

Railsのseedは、開発環境や本番環境での初期データの生成に適した仕組みですが、seedにはいくつかの欠点があります。

  • 複雑な依存関係を持つデータの生成には適していない

  • データのランダム生成には適していない

  • すでにデータが存在する場合、追加でデータ投入する用途には適していない

これらの欠点を解消するため、かつてはseed_fuというGemの使用が選択肢に挙がりました。しかしこのGemは2018年4月を最後にメンテナンスが止まっているため、新たに採用することは避けました。
そのため、seedを使うのは静的なマスタデータを入れる用途にのみ限定し、その他の場面ではRakeタスクというように使い分けています。


得られた効果と、今後の課題

動作確認データ生成処理を再設計し、以下の効果を得ることができました。

  • より動作確認に適したデータを生成できる体制ができた

  • 不適切なデータが生成されるリスクを排除できた

今後の課題は以下の通りです。

  • テーブルを追加するごとに、ダミーデータ生成タスクの実装が必要

    • テーブル追加の都度Rakeタスクを作ることは避けられないため、相応の運用コストがかかります。

  • FactoryBotのtraitの活用

    • データ生成クラスの中でgenerate_dummy_textのロジックを書いていますが、さらに改良するならば、FactoryBotのtraitを活用し、責務を分離することが考えられます。


おわりに

今回は既存のデータ生成処理を見直し、改善した事例を紹介しました。
ここまで読んでいただき、ありがとうございました。


▼ワンキャリアのエンジニア組織のことを知りたい方はまずこちら

▼カジュアル面談を希望の方はこちら

▼エンジニア求人票


この記事が参加している募集

オープン社内報

この記事が気に入ったらサポートをしてみませんか?