この記事について
株式会社Diverse Advent Calendar2021 20日目の記事になります!
Firestoreは投げられないQueryというのが存在してます。
その一つが集計関数であるGroupByです。
使用できないがサービスを組み合わせて同じように
表現することは可能だと考えてまして今回はこの内容について触れようと思います。
アプローチ内容
集計ができるデータストアを利用することです。
全体の内容としては以下です。
- 集計対象のCollectionにFirebaseExtension(Stream Collections to BigQuery)を設定しBigQueryへデータが流れるようにする。
- Cloud Schedulerで集計タイミングを設定しておく
- Cloud FunctionsでBigQueryへ集計Queryを投げ結果をFirestoreに入れる
ユースケース
例
やりたいこと: 直近30日間の商品の購入ランキングトップ10をユーザに表示したい
Firestoreのデータ構造
BigQueryのデータ構造
Stream Collections to BigQueryを使用した場合対象のDocument内容はdataカラムにjson形式で入ります。
{"product_id": 1,"createdAt": {"_seconds":1639720090,"_nanoseconds":181000000}}
Code
import { BigQuery } from '@google-cloud/bigquery';
interface SellRankingTableSchema {
product: string;
}
export const createSellRanking = functions
.region('asia-northeast1')
.pubsub.schedule('0 0 * * *')
.timeZone('Asia/Tokyo')
.onRun(async () => {
const bigQuery = new BigQuery();
const dataSetId = 'firestore_export';
const tableId = 'orders_raw_latest';
const column =
"JSON_EXTRACT_SCALAR(data, '$.product_id') AS product";
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() - 30);
const before30DaysUnixTime = Math.floor(currentDate.getTime() / 1000);
const condition = `CAST(JSON_EXTRACT_SCALAR(data, '$.createdAt._seconds') as INT64) >= ${before30DaysUnixTime} GROUP BY product ORDER BY COUNT(*) DESC LIMIT 10`;
const [rows] = await bigQuery.query(
`select ${column} from ${dataSetId}.${tableId} where ${condition}`
);
const sellRanking: string[] = rows.map(function(row) {
return (row as SellRankingTableSchema).product;
});
await admin
.firestore()
.doc('sellRanking')
.update({
products: sellRanking,
aggregateDate: admin.firestore.FieldValue.serverTimestamp(),
});
});
注意点
商品が購入されるたびに集計しない。(Createトリガー起点
BigQueryではQuery実行のたび費用が発生するため。
最後に
やろうと思えばBigQueryを使用せずCloud Functionsのロジックでカバーすることは可能です。
ただ、正直めんどくさい匂いは感じます。
なので簡単に実現できる方法であればBigQueryからデータを取得、整形し参照する FirestoreのDocumentにデータを入れることも実現手段の一つとして検討してみてはいかがでしょうか?