web-dev-qa-db-ja.com

Firestore:コレクション内のランダムなドキュメントを取得する方法

アプリケーションがfirebaseのコレクションから複数のドキュメントをランダムに選択できることが重要です。

Firebaseにはネイティブ機能が組み込まれていないため(これを知っている)、これを行うクエリを実現するために、最初に考えたのは、クエリカーソルを使用してランダムな開始インデックスと終了インデックスを選択することでした。コレクション。

このアプローチは機能しますが、限られた方法でのみ機能します。すべてのドキュメントは、隣接するドキュメントと一緒に順番に提供されるためです。ただし、親コレクションのインデックスでドキュメントを選択できた場合、ランダムなドキュメントクエリを実行できますが、問題は、これを行う方法を説明するドキュメントが見つからないことです。

これが私ができるようにしたいことです。次のfirestoreスキーマを考えてください:

root/
  posts/
     docA
     docB
     docC
     docD

次に、クライアントで(Swift環境)にいます)これを実行できるクエリを作成します。

db.collection("posts")[0, 1, 3] // would return: docA, docB, docD

とにかく私はこれに沿って何かをすることができますか?または、同様の方法でランダムなドキュメントを選択できる別の方法がありますか?

助けてください。

27
Garret Kaye

ランダムに生成されたインデックスと単純なクエリを使用して、Cloud Firestoreのコレクションまたはコレクショングループからドキュメントをランダムに選択できます。

この回答は4つのセクションに分かれており、各セクションに異なるオプションがあります。

  1. ランダムインデックスを生成する方法
  2. ランダムインデックスのクエリ方法
  3. 複数のランダムドキュメントの選択
  4. 継続的なランダム性の再シード

ランダムインデックスを生成する方法

この答えの基本は、インデックスフィールドを作成することです。このフィールドは、昇順または降順で並べ替えると、すべてのドキュメントがランダムに並べ替えられます。これを作成するにはさまざまな方法がありますので、2を見てみましょう。

自動IDバージョン

クライアントライブラリで提供されるランダムに生成された自動IDを使用している場合、この同じシステムを使用してドキュメントをランダムに選択できます。この場合、ランダムに順序付けられたインデックスはドキュメントIDです

クエリセクションの後半で生成するランダム値は、新しい自動ID( iOSAndroidWeb )とクエリするフィールドです。 ___name___フィールドであり、後述の「低い値」は空の文字列です。これは、ランダムインデックスを生成する最も簡単な方法であり、言語やプラットフォームに関係なく機能します。

デフォルトでは、ドキュメント名(___name___)はインデックスの昇順のみであり、削除および再作成以外の既存のドキュメントの名前を変更することもできません。これらのいずれかが必要な場合、このメソッドを使用して、この目的のためにドキュメント名をオーバーロードするのではなく、randomと呼ばれる実際のフィールドとしてauto-idを保存できます。

ランダム整数バージョン

文書を作成するときは、まず制限された範囲でランダムな整数を生成し、randomというフィールドとして設定します。予想されるドキュメントの数に応じて、異なる境界範囲を使用してスペースを節約したり、衝突のリスクを低減したりできます(これにより、この手法の有効性が低下します)。

さまざまな考慮事項があるため、必要な言語を検討する必要があります。 Swiftは簡単ですが、JavaScriptには特に注意すべき点があります。

  • 32ビット整数:小さい(〜10K 衝突する可能性は低い )データセットに最適
  • 64ビット整数:大きなデータセット(注:JavaScriptはネイティブにサポートしていません、 まだ

これにより、ドキュメントがランダムにソートされたインデックスが作成されます。クエリセクションの後半で、生成するランダム値はこれらの値の別の値になり、後述の「低い値」は-1になります。

ランダムインデックスのクエリ方法

ランダムインデックスができたので、クエリを実行します。以下では、1つのランダムドキュメントを選択するためのいくつかの簡単なバリアントと、1つ以上を選択するためのオプションを見ていきます。

これらのすべてのオプションについて、ドキュメントの作成時に作成したインデックス付きの値と同じ形式で、以下の変数randomで示される新しいランダム値を生成する必要があります。この値を使用して、インデックス上のランダムスポットを見つけます。

包み込む

ランダムな値を取得したので、1つのドキュメントを照会できます。

_let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
                   .order(by: "random")
                   .limit(to: 1)
_

これによりドキュメントが返されたことを確認します。そうでない場合は、再度クエリを実行しますが、ランダムインデックスには「低い値」を使用します。たとえば、ランダム整数を実行した場合、lowValueは_0_です。

_let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
                   .order(by: "random")
                   .limit(to: 1)
_

単一のドキュメントがある限り、少なくとも1つのドキュメントを返すことが保証されます。

双方向

ラップアラウンド方式は実装が簡単で、昇順インデックスのみを有効にしてストレージを最適化できます。 1つの欠点は、値が不当にシールドされる可能性です。たとえば、10Kのうち最初の3つのドキュメント(A、B、C)のランダムなインデックス値がA:409496、B:436496、C:818992の場合、AとCは選択される可能性が1/10K未満です。 Bは、Aの近接によって事実上シールドされ、おおよそ1/160Kのチャンスです。

単一の方向でクエリを実行し、値が見つからない場合は折り返すのではなく、代わりに_>=_と_<=_の間でランダムに選択することができます。インデックスストレージを2倍にします。

一方の方向が結果を返さない場合、もう一方の方向に切り替えます。

_queryRef = postsRef.whereField("random", isLessThanOrEqualTo: random)
                   .order(by: "random", descending: true)
                   .limit(to: 1)

queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
                   .order(by: "random")
                   .limit(to: 1)
_

複数のランダムドキュメントの選択

多くの場合、一度に複数のランダムドキュメントを選択する必要があります。上記の手法を調整するには、どのトレードオフが必要かによって2つの異なる方法があります。

すすぎと繰り返し

この方法は簡単です。毎回新しいランダムな整数を選択するなど、プロセスを繰り返します。

この方法では、同じパターンを繰り返し表示することを心配せずに、ドキュメントのランダムシーケンスが得られます。

トレードオフは、ドキュメントごとにサービスへの個別のラウンドトリップが必要になるため、次の方法よりも遅くなることです。

来てください

このアプローチでは、必要なドキュメントの制限内の数を増やすだけです。呼び出しで_0..limit_ドキュメントを返す可能性があるため、少し複雑です。次に、不足しているドキュメントを同じ方法で取得する必要がありますが、制限は差分のみに削減されます。求めている数よりも合計でドキュメントが多いことがわかっている場合は、2回目の呼び出しで(ただし1回目では)十分なドキュメントを取得できないというEdgeのケースを無視して最適化できます。

このソリューションとのトレードオフは、繰り返し行われます。文書はランダムに順序付けられますが、範囲が重複するようになった場合、前に見たのと同じパターンが表示されます。再播種に関する次のセクションで説明するこの懸念を軽減する方法があります。

このアプローチは、「Rinse&Repeat」よりも高速です。ベストケースですべてのドキュメントを要求するのは、シングルコールまたはワーストケース2コールです。

継続的なランダム性の再シード

このメソッドは、ドキュメントセットが静的な場合にドキュメントをランダムに提供しますが、各ドキュメントが返される確率も静的になります。一部の値は、取得した初期ランダム値に基づいて不当に低いまたは高い確率を持つ可能性があるため、これは問題です。多くのユースケースではこれで問題ありませんが、一部のドキュメントでは、1つのドキュメントが返される可能性をより均一にするために、長期ランダム性を高めたい場合があります。

挿入された文書は最終的に中間に織り込まれ、文書の削除と同様に確率が徐々に変化することに注意してください。文書の数を考慮して挿入/削除率が小さすぎる場合、これに対処するいくつかの戦略があります。

マルチランダム

再シードを心配するのではなく、ドキュメントごとに複数のランダムインデックスを常に作成し、それらのインデックスのいずれかを毎回ランダムに選択できます。たとえば、フィールドrandomをサブフィールド1〜3のマップにします。

_{'random': {'1': 32456, '2':3904515723, '3': 766958445}}
_

これで、random.1、random.2、random.3に対してランダムにクエリを実行し、ランダム性のより大きな広がりを作成します。これは本質的に、ストレージの増加と引き換えに、再シードする必要のある計算(ドキュメントの書き込み)を節約します。

書き込みのシード

ドキュメントを更新するたびに、randomフィールドのランダムな値を再生成します。これにより、ランダムインデックス内でドキュメントが移動します。

読み取りのシード

生成されたランダムな値が一様に分布していない場合(ランダムであるため、これが予想されます)、同じドキュメントが不適切な時間に選択される可能性があります。これは、ランダムに選択されたドキュメントが読み取られた後、新しいランダム値で更新することで簡単に対処できます。

書き込みはより高価で、ホットスポットになる可能性があるため、その時点のサブセット(たとえば、if random(0,100) === 0) update;)を読み取り時にのみ更新することを選択できます。

54
Dan McGrath

将来この問題を抱えている人を助けるためにこれを投稿します。

Auto IDを使用している場合、 Dan McGrath's Answer で説明されているように、新しいAuto IDを生成し、最も近いAuto IDを照会できます。

最近ランダム引用APIを作成し、Firestoreコレクションからランダム引用を取得する必要がありました。
これが私がその問題を解決した方法です。

var db = admin.firestore();
var quotes = db.collection("quotes");

var key = quotes.doc().id;

quotes.where(admin.firestore.FieldPath.documentId(), '>=', key).limit(1).get()
.then(snapshot => {
    if(snapshot.size > 0) {
        snapshot.forEach(doc => {
            console.log(doc.id, '=>', doc.data());
        });
    }
    else {
        var quote = quotes.where(admin.firestore.FieldPath.documentId(), '<', key).limit(1).get()
        .then(snapshot => {
            snapshot.forEach(doc => {
                console.log(doc.id, '=>', doc.data());
            });
        })
        .catch(err => {
            console.log('Error getting documents', err);
        });
    }
})
.catch(err => {
    console.log('Error getting documents', err);
});

クエリのキーは次のとおりです。

.where(admin.firestore.FieldPath.documentId(), '>', key)

また、ドキュメントが見つからない場合は、操作を逆にして再度呼び出します。

これがお役に立てば幸いです!
興味がある場合は、この特定の部分を見つけることができます my API on GitHub

8
ajzbc

Angular 7 + RxJSでこの作業を行ったので、例を求めている人とここで共有します。

@Dan McGrathの回答を使用して、次のオプションを選択しました。ランダムな整数バージョン+複数の数字のリンスとリピート。また、この記事で説明したものを使用しました: RxJS、If-Else演算子はどこですか? ストリームレベルでif/elseステートメントを作成します(そのいずれかの入門書が必要な場合)。

また、AngularでFirebaseを簡単に統合するために angularfire2 を使用したことに注意してください。

コードは次のとおりです。

import { Component, OnInit } from '@angular/core';
import { Observable, merge, pipe } from 'rxjs';
import { map, switchMap, filter, take } from 'rxjs/operators';
import { AngularFirestore, QuerySnapshot } from '@angular/fire/firestore';

@Component({
  selector: 'pp-random',
  templateUrl: './random.component.html',
  styleUrls: ['./random.component.scss']
})
export class RandomComponent implements OnInit {

  constructor(
    public afs: AngularFirestore,
  ) { }

  ngOnInit() {
  }

  public buttonClicked(): void {
    this.getRandom().pipe(take(1)).subscribe();
  }

  public getRandom(): Observable<any[]> {
    const randomNumber = this.getRandomNumber();
    const request$ = this.afs.collection('your-collection', ref => ref.where('random', '>=', randomNumber).orderBy('random').limit(1)).get();
    const retryRequest$ = this.afs.collection('your-collection', ref => ref.where('random', '<=', randomNumber).orderBy('random', 'desc').limit(1)).get();

    const docMap = pipe(
      map((docs: QuerySnapshot<any>) => {
        return docs.docs.map(e => {
          return {
            id: e.id,
            ...e.data()
          } as any;
        });
      })
    );

    const random$ = request$.pipe(docMap).pipe(filter(x => x !== undefined && x[0] !== undefined));

    const retry$ = request$.pipe(docMap).pipe(
      filter(x => x === undefined || x[0] === undefined),
      switchMap(() => retryRequest$),
      docMap
    );

    return merge(random$, retry$);
  }

  public getRandomNumber(): number {
    const min = Math.ceil(Number.MIN_VALUE);
    const max = Math.ceil(Number.MAX_VALUE);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
}

2
MartinJH

Angular + Firestoreを使用し、@ Dan McGrathのテクニックを基にしたものについては、コードスニペットをご覧ください。

以下のコードスニペットは1つのドキュメントを返します。

  getDocumentRandomlyParent(): Observable<any> {
    return this.getDocumentRandomlyChild()
      .pipe(
        expand((document: any) => document === null ? this.getDocumentRandomlyChild() : EMPTY),
      );
  }

  getDocumentRandomlyChild(): Observable<any> {
      const random = this.afs.createId();
      return this.afs
        .collection('my_collection', ref =>
          ref
            .where('random_identifier', '>', random)
            .limit(1))
        .valueChanges()
        .pipe(
          map((documentArray: any[]) => {
            if (documentArray && documentArray.length) {
              return documentArray[0];
            } else {
              return null;
            }
          }),
        );
  }

1).expand()は、ランダム選択からドキュメントを確実に取得するための再帰のためのrxjs操作です。

2)再帰が期待どおりに機能するためには、2つの別個の関数が必要です。

3)EMPTYを使用して.expand()演算子を終了します。

import { Observable, EMPTY } from 'rxjs';
0

Firebase Firestoreでリストドキュメントをランダムに取得する方法が1つありますが、それは本当に簡単です。 Firestoreにデータをアップロードするとき、1〜100万のランダムな値を持つフィールド名「位置」を作成します。 Fireストアからデータを取得すると、[位置]フィールドで[順序]を設定し、その値を更新します。多くのユーザーロードデータとデータは常に更新され、ランダムな値になります。

0
HVA Software