Firestore で いいね順（Score順）Sort + Paging するポイント

![1_HvVj0Bb8rAYugkibgRlp9w.png](https://qiita-image-store.s3.amazonaws.com/0/41793/54f95501-3fb4-df9c-c2c1-b1cfc4a0eee0.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](https://qiita-image-store.s3.amazonaws.com/0/41793/b379e007-c2b1-2700-474a-fb30286eca2c.png)

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

---

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

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

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

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

```ts
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)
```
# いいね順にソートする

ソートするだけなら簡単

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

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

ただこれを

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

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

# 複数の OrderBy を使う

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

```typescript
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  |...|

```typescript
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^