99 votes

Firestore : Comment obtenir des documents aléatoires dans une collection

Il est crucial pour mon application d'être en mesure de sélectionner plusieurs documents au hasard à partir d'une collection dans firebase.

Puisqu'il n'y a pas de fonction native intégrée à Firebase (à ma connaissance) pour réaliser une requête qui fait exactement cela, ma première idée était d'utiliser des curseurs de requête pour sélectionner un index de début et de fin aléatoire, à condition que je dispose du nombre de documents dans la collection.

Cette approche fonctionnerait mais seulement de façon limitée puisque chaque document serait servi en séquence avec les documents voisins à chaque fois ; cependant, si je pouvais sélectionner un document par son index dans sa collection parente, je pourrais obtenir une requête de documents aléatoire mais le problème est que je ne trouve aucune documentation qui décrit comment faire cela ou même si vous pouvez le faire.

Voici ce que j'aimerais pouvoir faire, en considérant le schéma suivant de firestore :

root/
  posts/
     docA
     docB
     docC
     docD

Ensuite, dans mon client (je suis dans un environnement Swift), j'aimerais écrire une requête qui puisse faire cela :

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

Est-ce que je peux faire quelque chose dans ce sens ? Ou bien, existe-t-il un autre moyen de sélectionner des documents aléatoires de manière similaire ?

Aidez-moi, s'il vous plaît.

1 votes

Une façon simple de récupérer des documents aléatoires est de rassembler toutes les clés des messages dans un tableau ( docA , docB , docC , docD ) puis mélange le tableau et récupère les trois premières entrées, de sorte que le mélange puisse retourner quelque chose comme docB , docD , docA .

1 votes

Ok, c'est une bonne idée ! Mais comment obtenir les clés de la poste ? Merci pour la réponse.

0 votes

J'espère que ce lien sera utile d'un point de vue logique : stackoverflow.com/a/58023128/1318946

169voto

Dan McGrath Points 9839

À l'aide d'index générés de manière aléatoire et de requêtes simples, vous pouvez sélectionner de manière aléatoire des documents dans une collection ou un groupe de collections dans Cloud Firestore.

Cette réponse est divisée en 4 sections avec différentes options dans chaque section :

  1. Comment générer les index aléatoires
  2. Comment interroger les index aléatoires
  3. Sélection de plusieurs documents aléatoires
  4. Le réensemencement pour un aléatoire permanent

Comment générer les index aléatoires

La base de cette réponse est la création d'un champ indexé qui, lorsqu'il est ordonné de manière ascendante ou descendante, donne lieu à un classement aléatoire de tous les documents. Il existe différentes façons de créer un tel champ. Nous allons donc en examiner deux, en commençant par la plus facilement accessible.

Version Auto-Id

Si vous utilisez les identifiants automatiques générés de manière aléatoire fournis dans nos bibliothèques clients, vous pouvez utiliser ce même système pour sélectionner un document de manière aléatoire. Dans ce cas, l'index aléatoirement ordonné est l'identifiant du document.

Plus loin dans notre section sur les requêtes, la valeur aléatoire que vous générez est un nouvel auto-id ( iOS , Android , Web ) et le champ que vous interrogez est le __name__ et la "faible valeur" mentionnée plus loin est une chaîne vide. Cette méthode est de loin la plus simple pour générer l'indice aléatoire et fonctionne quels que soient la langue et la plate-forme.

Par défaut, le nom du document ( __name__ ) n'est indexé que de manière ascendante, et vous ne pouvez pas non plus renommer un document existant sans le supprimer et le recréer. Si vous avez besoin de l'un ou l'autre de ces éléments, vous pouvez toujours utiliser cette méthode et stocker un auto-id comme un champ réel appelé random plutôt que de surcharger le nom du document à cette fin.

Version de Random Integer

Lorsque vous écrivez un document, générez d'abord un nombre entier aléatoire dans une plage délimitée et définissez-le comme un champ appelé random . En fonction du nombre de documents attendus, vous pouvez utiliser une plage délimitée différente pour gagner de la place ou réduire le risque de collisions (qui réduisent l'efficacité de cette technique).

Vous devez réfléchir aux langues dont vous avez besoin, car les considérations sont différentes. Alors que Swift est facile, JavaScript notamment peut avoir un problème :

  • Entier de 32 bits : Idéal pour les petits (~10K peu de chances d'avoir une collision%5En),%20m%3D2%5E32,%20n%3D10000) ) des ensembles de données
  • Entier de 64 bits : Grands ensembles de données (note : JavaScript ne supporte pas nativement, mais )

Cela créera un index avec vos documents triés de manière aléatoire. Plus tard, dans la section consacrée aux requêtes, la valeur aléatoire que vous générerez sera une autre de ces valeurs, et la "faible valeur" mentionnée plus loin sera -1.

Comment interroger les index aléatoires

Maintenant que vous avez un index aléatoire, vous allez vouloir l'interroger. Nous examinons ci-dessous quelques variantes simples pour sélectionner un document aléatoire, ainsi que des options pour en sélectionner plus d'un.

Pour toutes ces options, vous voudrez générer une nouvelle valeur aléatoire de la même forme que les valeurs indexées que vous avez créées lors de l'écriture du document, désignée par la variable random en dessous. Nous allons utiliser cette valeur pour trouver un endroit aléatoire sur l'index.

Wrap-around

Maintenant que vous avez une valeur aléatoire, vous pouvez demander un document unique :

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

Vérifiez que cette opération a renvoyé un document. Si ce n'est pas le cas, relancez la requête en utilisant la "faible valeur" de votre index aléatoire. Par exemple, si vous avez utilisé l'option "Random Integers", alors lowValue est 0 :

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

Tant que vous n'avez qu'un seul document, vous aurez la garantie de renvoyer au moins un document.

Bi-directionnel

La méthode wrap-around est simple à mettre en œuvre et permet d'optimiser le stockage avec seulement un index ascendant activé. Un inconvénient est la possibilité que des valeurs soient injustement protégées. Par exemple, si les trois premiers documents (A, B, C) sur 10 000 ont des valeurs d'index aléatoires de A:409496, B:436496, C:818992, alors A et C ont un peu moins de 1/10 000 de chances d'être sélectionnés, tandis que B est protégé par la proximité de A et n'a que 1/160 000 de chances.

Plutôt que d'effectuer une requête dans une seule direction et de tourner autour si une valeur n'est pas trouvée, vous pouvez sélectionner aléatoirement entre >= y <= qui réduit de moitié la probabilité de valeurs injustement protégées, au prix d'un stockage double de l'index.

Si une direction ne donne aucun résultat, passez à l'autre direction :

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)

Sélection de plusieurs documents aléatoires

Souvent, vous voudrez sélectionner plus d'un document aléatoire à la fois. Il existe deux façons différentes d'ajuster les techniques ci-dessus en fonction des compromis que vous souhaitez obtenir.

Rincer et répéter

Cette méthode est simple. Il suffit de répéter le processus, en sélectionnant à chaque fois un nouvel entier aléatoire.

Cette méthode vous permettra d'obtenir des séquences aléatoires de documents sans craindre de voir les mêmes motifs se répéter.

La contrepartie est qu'elle sera plus lente que la méthode suivante puisqu'elle nécessite un aller-retour distinct vers le service pour chaque document.

Continuez comme ça

Dans cette approche, il suffit d'augmenter le nombre dans la limite jusqu'aux documents souhaités. C'est un peu plus complexe, car vous pourriez rendre 0..limit documents dans l'appel. Vous devrez alors obtenir les documents manquants de la même manière, mais avec une limite réduite à la seule différence. Si vous savez qu'il y a plus de documents au total que le nombre que vous demandez, vous pouvez optimiser en ignorant le cas limite de ne jamais récupérer assez de documents au deuxième appel (mais pas au premier).

La contrepartie de cette solution réside dans la répétition des séquences. Bien que les documents soient ordonnés de manière aléatoire, si vous finissez par chevaucher des plages, vous verrez le même schéma que précédemment. Il existe des moyens d'atténuer ce problème, comme nous le verrons dans la section suivante sur le réensemencement.

Cette approche est plus rapide que le "rinçage et la répétition", car vous demanderez tous les documents en un seul appel dans le meilleur des cas ou en deux appels dans le pire des cas.

Le réensemencement pour un aléatoire permanent

Bien que cette méthode vous donne des documents de manière aléatoire, si l'ensemble de documents est statique, la probabilité que chaque document soit retourné sera également statique. C'est un problème car certaines valeurs peuvent avoir des probabilités injustement faibles ou élevées en fonction des valeurs aléatoires initiales qu'elles ont obtenues. Dans de nombreux cas d'utilisation, cela convient, mais dans certains cas, vous voudrez peut-être augmenter le caractère aléatoire à long terme pour avoir une chance plus uniforme de renvoyer un document.

Notez que les documents insérés finiront par s'intercaler, modifiant progressivement les probabilités, tout comme les documents supprimés. Si le taux d'insertion/suppression est trop faible compte tenu du nombre de documents, il existe quelques stratégies pour y remédier.

Multi-Random

Plutôt que de vous soucier du réensemencement, vous pouvez toujours créer plusieurs index aléatoires par document, puis sélectionner aléatoirement un de ces index à chaque fois. Par exemple, faites en sorte que le champ random soit une carte avec les sous-champs 1 à 3 :

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

Maintenant, vous ferez des requêtes sur random.1, random.2, random.3 de manière aléatoire, créant ainsi une plus grande dispersion de l'aléa. Cela revient à échanger une augmentation du stockage contre une augmentation du calcul (écritures de documents) pour avoir à réensemencer.

Réensemencement sur les écritures

Chaque fois que vous mettez à jour un document, vous devez générer à nouveau la ou les valeurs aléatoires de l'attribut random champ. Cela déplacera le document dans l'index aléatoire.

Répétition de la lecture

Si les valeurs aléatoires générées ne sont pas uniformément distribuées (elles sont aléatoires, donc on s'y attend), alors le même document peut être choisi un nombre disproportionné de fois. Il est facile de contrer ce phénomène en mettant à jour le document sélectionné au hasard avec de nouvelles valeurs aléatoires après sa lecture.

Étant donné que les écritures sont plus coûteuses et qu'elles peuvent avoir des points chauds, vous pouvez choisir de ne mettre à jour en lecture qu'un sous-ensemble du temps (par ex, if random(0,100) === 0) update; ).

1 votes

Merci Dan, j'apprécie vraiment votre réponse, mais en ce qui concerne la version agnostique (qui me semble la meilleure), si je voulais obtenir plus d'un document aléatoire, je devrais appeler cette requête plusieurs fois ? Ou augmenter la limite de la requête (qui renverrait des groupes aléatoires mais les documents de ces groupes seraient toujours dans la même séquence) ?

0 votes

Correct, ces deux options sont viables. La première (appels multiples) sera plus lente, mais conduira à une séquence moins répétée si elle est faite souvent. La seconde (limite plus large) sera rapide, mais augmentera les chances de revoir la même séquence. Notez qu'avec cette dernière, la séquence peut changer au fur et à mesure que des documents sont ajoutés. Vous pouvez également refaire le nombre aléatoire chaque fois que vous mettez à jour le document pour modifier davantage les séquences.

0 votes

Ah oui ! Je pense que cette méthode fera l'affaire. Merci, votre aide est très appréciée !

44voto

ajzbc Points 317

J'envoie ce message pour aider toute personne qui aurait ce problème à l'avenir.

Si vous utilisez des auto-identifiants, vous pouvez générer un nouvel auto-identifiant et rechercher l'auto-identifiant le plus proche, comme indiqué dans l'article suivant La réponse de Dan McGrath .

J'ai récemment créé une api de citations aléatoires et j'avais besoin d'obtenir des citations aléatoires à partir d'une collection de firestore.
Voici comment j'ai résolu ce problème :

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);
});

La clé de la requête est la suivante :

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

Et l'appeler à nouveau avec l'opération annulée si aucun document n'est trouvé.

J'espère que cela vous aidera !
Si vous êtes intéressé, vous pouvez trouver cette partie spécifique de mon API sur GitHub

7 votes

Il est très peu probable que vous rencontriez ce problème avec les identifiants de documents, mais au cas où quelqu'un copierait cette méthode et l'utiliserait avec un espace d'identification beaucoup plus petit, je recommanderais de changer la première clause where de '>' à '>='. Cela permet d'éviter un échec dans le cas limite où il n'y a qu'un seul document, et que key est sélectionné de manière à être exactement l'id du document 1.

2 votes

Merci pour l'excellente réponse que vous avez publiée ici. J'ai une question : à quoi fait référence 'admin.firestore.FieldPath.documentId()' exactement ?

2 votes

J'utilise Flutter et cela ne permet pas d'obtenir un document de manière aléatoire. Il a un pourcentage élevé de chances de retourner le même document. En fin de compte, il obtient des documents au hasard, mais dans 90 % des cas, il s'agit du même document.

4voto

MartinJH Points 1593

Je viens de le faire fonctionner en Angular 7 + RxJS, et je le partage ici avec les personnes qui veulent un exemple.

J'ai utilisé la réponse de @Dan McGrath, et j'ai choisi ces options : Random Integer version + Rinse & Repeat pour les nombres multiples. J'ai aussi utilisé les trucs expliqués dans cet article : RxJS, où est l'opérateur If-Else ? pour faire des déclarations if/else au niveau du flux (si vous avez besoin d'une introduction à cela).

Notez également que j'ai utilisé feu angulaire2 pour une intégration facile de Firebase dans Angular.

Voici le code :

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 votes

Pour les futurs lecteurs : J'ai mis à jour ma réponse pour plus de clarté et renommé la section "Version agnostique de l'identité du document" en "Version de l'entier aléatoire".

2 votes

J'ai mis à jour ma réponse pour correspondre à vos changements.

1 votes

Solution très soignée. Super, mais où dans votre code faites-vous le rinçage et la répétition pour plusieurs numéros ?

0voto

HVA Software Points 11

J'ai une façon d'obtenir un document de liste aléatoire dans Firebase Firestore, c'est vraiment facile. Lorsque je télécharge des données sur Firestore, je crée un champ nommé "position" avec une valeur aléatoire de 1 à 1 million. Lorsque je reçois des données de Firebase Firestore, je vais définir l'ordre par le champ "Position" et mettre à jour la valeur de celui-ci, beaucoup d'utilisateurs chargent des données et les données sont toujours mises à jour et ce sera une valeur aléatoire.

0 votes

Bonne solution mais je vais ajouter inutilement plus d'opérations Firestore.

0 votes

@HimanshuRawat Vous avez raison, si votre application a une grande base d'utilisateurs, elle peut avoir un impact énorme.

0voto

choopage - Jek Bao Points 1540

Pour ceux qui utilisent Angular + Firestore, en s'appuyant sur les techniques de @Dan McGrath, voici l'extrait de code.

L'extrait de code ci-dessous renvoie 1 document.

  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() est une opération rxjs pour la récursion afin de s'assurer que nous obtenons bien un document à partir de la sélection aléatoire.

2) Pour que la récursion fonctionne comme prévu, nous devons avoir deux fonctions distinctes.

3) Nous utilisons EMPTY pour terminer l'opérateur .expand().

import { Observable, EMPTY } from 'rxjs';

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X