【Firebase】Firestoreの複合インデックスを作成しなければいけなかった実装を振り返る
背景
商品は在庫のstatusを保持しているのですが、その在庫のstatusで表示・非表示のハンドリングも行っていました。(例えばstatusが終売なら非表示、販売中なら表示にするように)
しかし「SEOの観点から終売だが表示したい」や「期間限定で商品を表示したい・季節のイベントの週だけEUに商品を表示したい(販売中だが非表示にしたい)」ニーズが出てきて、在庫のstatusで表示・非表示のハンドリングをするのではなく表示設定のフィールド(isHidden)を追加し責務を剥がす必要がありました。
手順
- Firestoreの該当ドキュメントに表示設定のフィールド(isHidden)を追加する
- statusからisHiddenに変更した複合クエリにする
上記の手順に沿って進めていたのですが手順2番目の複合クエリの改修時に複合インデックスの作成が必要になりました。
今回初見でしたので複合インデックスについて調査・検証した内容をメインに説明していきます。
Firestoreの該当ドキュメントに表示設定のフィールド(isHidden)を追加する
Firestoreの該当ドキュメントに表示設定のフィールド(isHidden)を追加します。
書き込み処理は500までという制限があるのでバッチ処理を採用しました。
筆者のやりたかったことは次の記事に記載されていたので添付させていただきます。
statusからisHiddenに変更した複合クエリにする
バッチ処理でisHiddenフィールドを追加し下記のように複合クエリを改修したところコンテンツが表示されなくなってしまいました。
const query = collection
.where('category', '==', 'drink')
.where('status', '==', 'sale') // where('isHidden', '==', false)に変更する
.orderBy('createdAt', 'desc');
クエリを発行したが該当のインデックスが登録されていない場合、errorになって結果が一切取得できないようです。
エラー内容を確認すると「そのクエリにはインデックスが必要なので、ここから作ってください」とリンクつきメッセージが表示されています。
丁寧にFirebaseコンソールへのリンクが貼られているので、ここから不足しているインデックスを作成します。
以降はメッセージの案内に従うだけで期待値の複合インデックスの作成ができます。
※インデックスの作成時間が多少かかるので即時反映はできないことに注意してください。
複合インデックス(Composite Index)とは
Firestoreの複合インデックス(Composite Index)は、データベース内の複数のフィールドを組み合わせた検索に使用されるインデックスです。
「索引(インデックス)」を作っておくとデータを検索するときに処理が早くなります。
インデックスは、データベースのパフォーマンスにおける重要な要素です。書籍内のトピックをページ番号に対応付ける書籍の索引(インデックス)と同様に、データベースのインデックスはデータベース内のアイテムをデータベース内の場所にマッピングします。データベースにクエリが送信される際、データベースはインデックスを使用して、リクエストされたアイテムの場所をすばやく検索します。
複合インデックスを使用することで、特定のフィールドの値だけでなく、他のフィールドの値も組み合わせて検索条件として指定できます。
例えばユーザーのコレクションがあり、名前と年齢のフィールドがあるとします。
名前で検索する場合は単一フィールドのインデックスで十分ですが、名前と年齢の組み合わせで検索する場合は、複合インデックスが必要になります。
また、複合インデックスを作るメリットとして「ジグザグマージ結合」によるパフォーマンスの改善が挙げられます。
※「ジグザグマージ結合」に関しては下記参照
さらにインデックスは自動でFirebaseが作ってくれます(ドキュメントでは「すべてのクエリの背後にインデックスが存在する」と表現されている)が複合インデックスは自分で作る必要があります。
そのため今回のエラーは複合インデックスを自分で作っていなかったことが原因でした。
クエリの制限事項についてまとめる
エラーメッセージの通り複合クエリの改修時に「複合インデックス」を作成をしていなかったため今回のエラーが発生しましたが、クエリの制限事項によっては複合インデックスの作成が不要か必要か分かれます。
インデックスの登録が不要な場合
下記のような簡単なクエリの場合、インデックスの登録は不要になります。
citiesRef.where("state", "==", "CA")
citiesRef.where("population", "<", 100000)
citiesRef.where("regions", "array-contains", "west_coast")
Firestore は単一フィールド インデックスを自動的に作成するため、アプリケーションでは最も基本的なデータベース クエリをすぐにサポートできます。単一フィールド インデックスを使用すると、フィールドの値と比較演算子 <、<=、==、>=、>、in に基づく単純なクエリを実行できます。配列フィールドについては array-contains クエリと array-contains-any クエリを実行できます。
等価演算子(==)に基づく複合クエリもインデックスの登録は不要になります。
citiesRef.where("state", "==", "CO").where("name", "==", "Denver")
citiesRef.where("country", "==", "USA").where("capital", "==", false)
インデックスの登録が必要な場合
しかし比較演算子(>, <=など)を使用する複合クエリや、別のフィールドでクエリをソート(orderBy)することはできないので複合インデックスを作成する必要があります。
今回のケースに当てはまりますね。
citiesRef.where("country", "==", "USA").orderBy("population", "asc")
citiesRef.where("country", "==", "USA").where("population", "<", 3800000)
citiesRef.where("country", "==", "USA").where("population", ">", 690000)
インデックスのマージ
先ほどのエラーメッセージから複合インデックスのページに遷移しFirebase側が必要なインデックスを用意してくれますが、毎回複合インデックスを作成してしまうといずれインデックスの制限に到達してしまうのでインデックスのマージで制限数の節約を行います。
今回のstatusからisHiddenの変更は等式が使われているので複合クエリ全ての登録をして複合インデックスを作るのではなく、isHiddenのインデックスを登録するだけで済みました。
Firestore ではすべてのクエリにインデックスを使用しますが、クエリごとに必ず 1 つのインデックスが必要になるわけではありません。複数の等式(==)句(およびオプションで orderBy 句)が含まれるクエリに対しては、Firestore は既存のインデックスを再利用できます。Firestore では、シンプルな等式フィルタのインデックスをマージして、より大規模な等式クエリに必要な複合インデックスを作成できます。
firestore.indexes.jsonに反映させる(調査のみ)
今回は短納期だったのとインデックスを登録する量が少なかったので手動でFirebaseコンソールから複合インデックスを作成をしましたがfirestore.indexes.jsonを使ってFirestoreに複合インデックスを作成することもできます。
確かにdev, stg, prodがあるとして毎回各環境のコンソールで手動追加するのも大変なのでfirestore.indexes.jsonにインデックス情報を各環境に反映するだけでよいので追加するのもできると楽になりそうです。
{
"indexes": [
{
"collectionGroup": "products",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "cetegory",
"order": "ASCENDING"
},
{
"fieldPath": "isHidden",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
}
]
}
最後に
表示設定のフィールド(isHidden)を追加し在庫状況のフィールド(status)が兼務していた責務を剥がすことができました。
複合インデックス周りの調査ではかなり類似の記事を書かれている方もいらっしゃったのでn番煎じな記事になってしまいましたが個人的なユースケースを踏まえどのような時にfirestoreの〇〇な機能を使うのかと整理することができました。
駆け足になりましたが、最後までご覧いただきありがとございました。
Discussion