JavaScript
Firestore
CloudFirestore

Firestore で いいね順(Score順)Sort + Paging するポイント

1_HvVj0Bb8rAYugkibgRlp9w.png

本記事のゴール

Firestore の Query で、下記形式のデータを対象にした いいね順(スコア順)ソート & ページング を実現します
なお、記載する例は 無限スクロール 用のページングを想定しています

articles Collection

... likeCount createdAt ...
... 42 12/1 10:01 ...
... 1 12/2 11:14 ...
... 24 12/4 07:20 ...
... 8 12/6 10:03 ...
... 24 12/8 23:02 ...
... 8 12/11 19:30 ...
... 100 12/16 17:30 ...
... 8 12/18 03:28 ...

↓ (各ページ2件 でページングする場合はこうしたい

page: 1

... likeCount createdAt ...
... 100 12/16 17:30 ...
... 42 12/1 10:01 ...

page: 2

... likeCount createdAt ...
... 24 12/8 23:02 ...
... 24 12/4 07:20 ...

page: 3

... likeCount createdAt ...
... 8 12/18 03:28 ...
... 8 12/11 19:30 ...

page: 4

... likeCount createdAt ...
... 8 12/6 10:03 ...
... 1 12/2 11:14 ...

まずは普通にページング

Firestore でのページングには 新しく追加された Date 型のフィールドを使っている方が多いのではないでしょうか。
Date 型を利用したフィールドは、管理画面上から わかりやすい表記で確認できます。

スクリーンショット 2017-12-14 22.35.13.png

Realtime DB 時代に使っていた timestamp 値 と比較すると、わざわざ変換せずに作成日等の情報が確認できるため便利です。


Firestore での Date 型を利用したページングは非常に簡単です。
単に基準とするフィールド(今回は createdAt)で並び替えた後、 初回なら適当に大きな Date、2 ページ目からは 最後に取得したアイテムの値を startAfter に渡すだけです。

const lastDate: Date = lastItem.createdAt;
const articlesRef = db.collection('articles');

articlesRef.orderBy('createdAt', 'desc').startAfter(lastDate).limit(10);

なお、startAfter と似た startAt というメソッドもありますが、両者の違いは渡した値を検索に含むか、含まないかです

lastDate:Date = new Date('2017 12/1 10:00');

// 検索範囲 > 2017 12/1 10:00 < 検索範囲
.orderBy('createdAt', 'asc' | 'desc').startAfter(lastDate)

// 検索範囲 ≧ 2017 12/1 10:00 ≦ 検索範囲
.orderBy('createdAt', 'asc' | 'desc').startAt(lastDate)

いいね順にソートする

ソートするだけなら簡単

const lastCount: number = lastItem.likeCount;
const articlesRef = db.collection('articles');

articlesRef.orderBy('likeCount', 'desc').limit(10);

ただこれを

articlesRef.orderBy('likeCount', 'desc').startAfter(lastCount).limit(10);

通常のページング処理と同様に行うと いいね数 が同じスコアの記事が次に検索する対象から外れてしまいます。
だからと言って startAt を使っても、今度は最終アイテムと同じスコアが重複してしまいます。
順位がついているランキングデータではこの Query のまま使えるでしょうが、今回の要件は実現できません。

複数の OrderBy を使う

この問題を解決するために、 いいね順 及び 作成日順 でソートを行います。
これにより、いいね が同数の場合は日付を参照して次のデータを重複せずに取得することができます。

const lastDate: Date = lastItem.createdAt;
const lastCount: number = lastItem.likeCount;
const articlesRef = db.collection('articles');

lastDate.setMilliseconds(lastDate.getMilliseconds() - 1);

articlesRef.orderBy('likeCount', 'desc').orderBy('createdAt', 'desc').startAt(lastCount, lastDate).limit(10);

ポイントをまとめると・・・

  • ページにまたがるアイテムのいいね数が重複することもあるので、 startAt で最終アイテムと同じいいね数から検索開始
  • 使用した orderBy 順に startAt の値を指定する
  • 今回は更新順の取得 => createdAt の値が下がっていく方向に進むので、開始日時を 1ms 減算

articles Collection

... likeCount createdAt ...
... 42 12/1 10:01 ...
... 1 12/2 11:14 ...
... 24 12/4 07:20 ...
... 8 12/6 10:03 ...
... 24 12/8 23:02 ...
... 8 12/11 19:30 ...
... 100 12/16 17:30 ...
... 8 12/18 03:28 ...
articlesRef.orderBy('likeCount', 'desc').orderBy('createdAt', 'desc').startAt(lastCount, lastDate).limit(2)

page: 1 ( likeCount: 大きな値 && createdAt: 大きな値 ~)

... likeCount createdAt ...
... 100 12/16 17:30 ...
... 42 12/1 10:01 ...


page: 2 ( likeCount: 42 以下のスコア && createdAt: 12/1 10:01 - 1ms 以下の時間 ~ )

... likeCount createdAt ...
... 24 12/8 23:02 ...
... 24 12/4 07:20 ...

page: 3 ( likeCount: 24 以下のスコア && createdAt: 12/4 07:20 - 1ms 以下の時間 ~ )

... likeCount createdAt ...
... 8 12/18 03:28 ...
... 8 12/11 19:30 ...

page: 4 ( likeCount: 8 以下のスコア && createdAt: 12/11 19:30 - 1ms 以下の時間 ~ )

... likeCount createdAt ...
... 8 12/6 10:03 ...
... 1 12/2 11:14 ...

おわり

上記のポイントを踏まえて実装することで 冒頭で述べたいいね数順のページングが実装できます (o・∇・)o
Realtime DB の頃は、こういった処理を実現するために予めリストデータを作成しておく必要があり、それの運用・管理が大変でした…^^;
一方で Firestore を利用するとこんなに簡単に実装できてしまいます。

最高です (´▽`)

日記

firestore の記事を書く際に Firestore タグ と CloudFirestore タグ どっちを付けるべきなのか迷いますね…
件数が多いのは Firestore だけど アイコンが付いてて本物っぽいのが CloudFirestore ...

う〜ん

とりあえずどっちもつけとこ^q^