Edited at

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

More than 1 year has passed since last update.

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^