👑

BigQureyから取得したデータをRailsアプリケーションでキャッシュして利用する

2023/12/25に公開

GoogleのBigQueryに保存されているデータを活用してランキング機能を作成したかった為、RailsのGemにある google-cloud-bigquery を使ってBigQueryから必要なデータの取得を行いました。
その際に、毎回同じリクエストを叩くとその度に料金が発生してしまい非常に勿体無いため、Railsに備わっているキャッシュ機能と組み合わせて、最低限のリクエストで済むようにしました。

google-cloud-bigqueryの使い方

google-cloud-bigquery は、Googleが公式で提供してくれているGemなので、こちらの公式ドキュメントを参考に導入することができます。

gem install google-cloud-bigquery

https://cloud.google.com/bigquery/docs/reference/libraries?hl=ja#client-libraries-install-ruby

Gemのインストールが完了しましたら、下記のようにライブラリを読み込んだのちに、BigQueryにアクセスするためのインスタンスを作成します。

require "google/cloud/bigquery"

query = Google::Cloud::Bigquery.new(
  project: "your-project-id", 
  credentials: "path/to/your/credentials.json"
)

次に、実行するクエリを定義します。
例えば、 your_dataset.your_table というテーブルから nameage を取得したい場合は以下のようにクエリを定義して変数に格納します。

sql = "SELECT name, age FROM `your_dataset.your_table` LIMIT 10"

最後に、作成したインスタンスのqueryメソッドの引数に先ほど定義して格納したクエリ変数を入れて実行すると、BigQueryにアクセスして、 name と age の値を取得することができます。

results = bigquery.query sql

results.each do |row|
  puts "名前: #{row[:name]}, 年齢: #{row[:age]}"
end

google-cloud-bigqueryのメソッド紹介

Gemの google-cloud-bigqery には、BigQueryでクエリを実行するための色々なメソッドがあるため、そちらのご紹介もします。

小規模データを同期的に取得する

結果のサイズが10MB以下の小規模なものであれば、 query メソッドを利用することで同期的にデータを取得することができます。

results = bigquery.query("SELECT name, age FROM `your_dataset.your_table`")

大規模データを非同期で取得する

大規模なクエリや長時間実行されるクエリの場合は、 create_query_job メソッドの利用がおすすめです。

job = bigquery.create_query_job("SELECT name, age FROM `your_dataset.your_table`")
results = job.query_results

このメソッドを利用することで、データ処理のためのジョブを生成してバックグラウンドで実行してくれます。
これにより、大規模なデータや複雑なクエリの実行時に、プログラムの実行をブロックすることなく処理を進めることが可能になります。

クエリジョブの確認をしたい場合

すでに実行されているクエリジョブの状態を確認したい場合は、query_job メソッドを利用することで確認することができます。

job = bigquery.query_job(job_id)

こちらのメソッドを利用することで、ジョブが完了したか、まだ実行中かなど状態を確認できたり、ジョブの実行中にエラーが発生した場合はそのエラーの詳細を取得することも可能です。
また、ジョブの実行にかかった時間や処理したデータ量なども取得可能です。

job_id = "your_job_id"

job = bigquery.query_job(job_id)

# ジョブの状態を確認
if job.done?
  puts "ジョブは正常に完了しました"
else
  puts "ジョブは実行中です"
end

# エラー情報の確認
if job.failed?
  puts "ジョブの実行中にエラーになりました: #{job.error}"
end

非同期で実行されたクエリの結果を取得する

create_query_job メソッドで非同期でクエリが実行された後に、その結果を取得するには query_results を使用します。
このメソッドを使用することで、非同期で実行されたクエリの結果を取得することが可能になります。

ただしquery_results メソッドで非同期データの結果を取得する場合は、必ずジョブが完了していることを確認した後に行う必要があるため、そこだけ注意が必要です。
job.wait_until_done! でジョブの完了を待つことができるため、下記のように使用できます。

job = bigquery.create_query_job("SELECT name, age FROM `your_dataset.your_table`")

# ジョブの完了を待つ
job.wait_until_done!

# ジョブが完了した後、結果を取得
results = bigquery.query_results(job.job_id)

以上が google-cloud-bigquery のメソッドの紹介になります。

Railsのキャッシュ機能と組み合わせて使う

Railsにある cache メソッドを利用することで、Railsにデフォルトで備わっているキャッシュストアにキャッシュされたデータを保存することができます。

Rails.cache.fetch("some_key") do
  # ここには重い計算やデータベースクエリなどが入る
end

この cache メソッドを利用してクエリの実行をキャッシュすることで、BigQueryからデータを取得する際に発生するコストを抑えることができます。

Rails.cache.fetch

cache メソッドも様々な使い方ができますが、今回は cache メソッドの中の fetch メソッドを利用しました。
このメソッドを利用すると、キャッシュストアを参照した際に、リクエストされたkeyと同じkeyデータがあればそのkeyに対応するキャッシュのデータを返します。
もし同じkeyが存在しなけれな、新たにそのkeyでキャッシュデータを作成してくれます。

キャッシュの有効期限を決める

fetchメソッドの第二引数で、 expires_in: 時間 でキャッシュの有効期限を指定することができます。
例えば1時間の有効期限を指定する場合は以下のように指定します。

Rails.cache.fetch("some_key", expires_in: 1.hour) do
  # クエリなどの結果を計算するコードが入る
end

あまりにも古いキャッシュを使用し続けるのはパフォーマンス的によろしくないため、適切な有効期限を決める必要があります。

Railsキャッシュ機能の詳細に関しては公式ドキュメントをご参照ください。
https://railsguides.jp/caching_with_rails.html

実際の使用例

require "google/cloud/bigquery"

class Api::SearchRankingToolsController < Api::BaseApiController
  def index
    results = fetch_data_and_create_sql(search_ranking_tool_params)
    cateogry_db_id = get_category_db_id(search_ranking_tool_params[:template_name])
    res = fetch_category_db_data(cateogry_db_id, search_ranking_tool_params)
    render json: results.map { |result| Api::SearchRankingToolSerializer.serialize(result, res) }, status: :ok
  end

  private

  def search_ranking_tool_params
    params.permit(:template_name, :limit)
  end

  def fetch_data_and_create_sql(params)
    cache_key = "#{params[:template_name]}_#{params[:limit]}"
    search_ranking_result = Rails.cache.fetch(cache_key, expires_in: 1.day) do
      query = Google::Cloud::Bigquery.new(project: Settings.spartan.project_id, credentials: Settings.google_cloud.damrey_keyfile_path)
      # SQLインジェクション対策のため、paramsを直接埋め込まずに、プレースホルダーを使ってSQLを組み立てる
      params = {
        template_name: params[:template_name],
        limit: params[:limit].to_i
      }
      sql = %(
        WITH word_data AS (
            SELECT SPLIT(data_value, ' ') AS word_list
            FROM `production.hogehoge.event_logs`
            WHERE _PARTITIONDATE BETWEEN DATE_SUB(DATE(CURRENT_TIMESTAMP(), "UTC"), INTERVAL 7 DAY)
                                      AND DATE_SUB(DATE(CURRENT_TIMESTAMP(), "UTC"), INTERVAL 1 DAY)
            AND data_key = @template_name
        ),
        unnested_word_data AS (
            SELECT word
            FROM word_data,
            UNNEST(word_list) AS word
        )
        SELECT word, COUNT(*) as word_search_count
        FROM unnested_word_data
        GROUP BY word
        ORDER BY word_search_count DESC
        LIMIT @limit
      );
      tmp_result = query.query sql, params: params

      # 必要なデータのみを抽出してシリアライズ可能な形式に変換
      tmp_result.map do |row|
        { word: row[:word], word_search_count: row[:word_search_count] }
      end
    end

    return search_ranking_result
  end

  def fetch_category_db_data(category_db_id, params)
    Rails.cache.fetch((params[:template_name]).to_s, expires_in: 1.hour) do
      Category.find(category_db_id).records.published
    end
  end

  def get_category_db_id(template_name)
    case template_name
    when "dqm3_synthetic_monster_search"
      return 18739
    when "dqm3_reverse_synthetic_monster_search"
      return 18739
    when "dqm3_encyclopedia_search_name"
      return 18739
    when "dqm3_move_list_search_name"
      return 18748
    when "dqm3_skill_search_to_suggest_skill_name"
      return 18747
    else
      return nil
    end
  end
end

BigQueryから取得したデータをシリアライザーへ渡してデータを成形する場合、BigQueryから取得したデータはそのまま渡すと不要なデータが紛れて上手く機能しないため、必要なデータのみを抽出する処理を挟んでデータをシリアライズ可能か形式に変換しています。

また、SQLインジェクションの対応も必要なため、paramsでパラメータ化してそれをqueryメソッドの第二引数に渡して対応しています。
参考記事: https://zenn.dev/aldagram_tech/articles/sql-injection-with-big-query

出力データ

クエリメソッドでBigQueryから取得したデータの結果が下記になります。

このデータを活用して、最終的に下記のようなランキング機能を作成しました。

まとめ

以上で、 BigQureyから取得したデータをRailsアプリでキャッシュして利用する方法の紹介を終わります。
Railsの cache は利用したことがありましたが、 google-cloud-bigquery は今回初めて利用してみてとても便利に感じたため、 BigQueryとRailsアプリケーションを組み合わせて何か作成したい場合はぜひ活用してみてください。

最後まで読んでいただきありがとうございました。
本記事が少しでも誰かのお役に立てれていれば幸いです。

ゲームエイトテックブログ

Discussion