Saltar al contenido

Firestore: Cómo obtener documentos aleatorios en una colección

Por fin después de mucho batallar hemos encontrado la solución de esta pregunta que agunos lectores de este sitio presentan. Si tienes algo que aportar puedes compartir tu comentario.

Solución:

Con índices generados aleatoriamente y consultas simples, puedes seleccionar documentos aleatoriamente de una colección o grupo de colecciones en Cloud Firestore.

Esta respuesta se divide en 4 secciones con diferentes opciones en cada sección:

  1. Cómo generar los índices aleatorios
  2. Cómo consultar los índices aleatorios
  3. Seleccionar varios documentos aleatorios
  4. Resiembra para aleatoriedad continua

Cómo generar los índices aleatorios

La base de esta respuesta es la creación de un campo indexado que, cuando se ordena de forma ascendente o descendente, da como resultado que todo el documento se ordene aleatoriamente. Hay diferentes formas de crear esto, así que veamos 2, comenzando con la más disponible.

Versión de identificación automática

Si está utilizando los identificadores automáticos generados aleatoriamente que se proporcionan en nuestras bibliotecas cliente, puede utilizar este mismo sistema para seleccionar un documento de forma aleatoria. En este caso, el índice ordenado aleatoriamente es la identificación del documento.

Más adelante en nuestra sección de consulta, el valor aleatorio que genera es una nueva identificación automática (iOS, Android, Web) y el campo que consulta es el __name__ campo, y el ‘valor bajo’ mencionado más adelante es un campo vacío string. Este es, con mucho, el método más fácil para generar el índice aleatorio y funciona independientemente del idioma y la plataforma.

De forma predeterminada, el nombre del documento (__name__) solo se indexa de forma ascendente, y tampoco puede cambiar el nombre de un documento existente a menos que se elimine y se vuelva a crear. Si necesita alguno de estos, aún puede usar este método y simplemente almacenar una identificación automática como un campo real llamado random en lugar de sobrecargar el nombre del documento para este propósito.

Versión de entero aleatorio

Cuando escriba un documento, primero genere un número entero aleatorio en un rango limitado y configúrelo como un campo llamado random. Dependiendo de la cantidad de documentos que espere, puede usar un rango limitado diferente para ahorrar espacio o reducir el riesgo de colisiones (que reducen la efectividad de esta técnica).

Debe considerar qué idiomas necesita, ya que habrá diferentes consideraciones. Si bien Swift es fácil, JavaScript notablemente puede tener un problema:

  • Entero de 32 bits: ideal para conjuntos de datos pequeños (~ 10K es poco probable que tengan una colisión)
  • Entero de 64 bits: grandes conjuntos de datos (nota: JavaScript no es compatible de forma nativa, todavía)

Esto creará un índice con sus documentos ordenados al azar. Más adelante en nuestra sección de consulta, el valor aleatorio que genere será otro de estos valores, y el ‘valor bajo’ mencionado más adelante será -1.

Cómo consultar los índices aleatorios

Ahora que tiene un índice aleatorio, querrá consultarlo. A continuación, analizamos algunas variantes simples para seleccionar 1 documento aleatorio, así como opciones para seleccionar más de 1.

Para todas estas opciones, querrá generar un nuevo valor aleatorio en la misma forma que los valores indexados que creó al escribir el documento, indicado por la variable random debajo. Usaremos este valor para encontrar un lugar aleatorio en el índice.

Envolver alrededor

Ahora que tiene un valor aleatorio, puede consultar un solo documento:

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

Comprueba que este haya devuelto un documento. Si no es así, vuelva a consultar pero use el ‘valor bajo’ para su índice aleatorio. Por ejemplo, si hiciste números enteros aleatorios, entonces lowValue es 0:

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

Siempre que tenga un solo documento, se le garantiza que devolverá al menos 1 documento.

Bidireccional

El método integral es fácil de implementar y le permite optimizar el almacenamiento con solo un índice ascendente habilitado. Una desventaja es la posibilidad de que los valores se protejan injustamente. Por ejemplo, si los primeros 3 documentos (A, B, C) de 10K tienen valores de índice aleatorios de A: 409496, B: 436496, C: 818992, entonces A y C tienen menos de 1 / 10K de posibilidades de ser seleccionados, mientras que B está efectivamente protegido por la proximidad de A y solo una probabilidad aproximada de 1 / 160K.

En lugar de realizar consultas en una sola dirección y pasar por alto si no se encuentra un valor, puede seleccionar aleatoriamente entre >= y <=, que reduce a la mitad la probabilidad de valores protegidos injustamente, a costa de duplicar el almacenamiento del índice.

Si una dirección no arroja resultados, cambie a la otra dirección:

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)

Seleccionar varios documentos aleatorios

A menudo, querrá seleccionar más de un documento aleatorio a la vez. Hay 2 formas diferentes de ajustar las técnicas anteriores según las compensaciones que desee.

Enjuague y repita

Este método es sencillo. Simplemente repita el proceso, incluida la selección de un nuevo entero aleatorio cada vez.

Este método le proporcionará secuencias aleatorias de documentos sin preocuparse por ver los mismos patrones repetidamente.

La compensación es que será más lento que el siguiente método, ya que requiere un viaje de ida y vuelta al servicio por separado para cada documento.

Sigue viniendo

En este enfoque, simplemente aumente el número en el límite de los documentos deseados. Es un poco más complejo, ya que podrías volver 0..limit documentos en la convocatoria. Luego, deberá obtener los documentos que faltan de la misma manera, pero con el límite reducido solo a la diferencia. Si sabe que hay más documentos en total que el número que está solicitando, puede optimizar ignorando el caso extremo de no obtener nunca suficientes documentos en la segunda llamada (pero no en la primera).

La compensación con esta solución es en secuencias repetidas. Si bien los documentos se ordenan al azar, si alguna vez termina superponiendo rangos, verá el mismo patrón que vio antes. Hay formas de mitigar esta preocupación que se analizan en la siguiente sección sobre resiembra.

Este enfoque es más rápido que "Enjuagar y repetir", ya que solicitará todos los documentos en el mejor de los casos una sola llamada o en el peor de los casos 2 llamadas.

Resiembra para aleatoriedad continua

Mientras esto el método da documentos al azar si el conjunto de documentos es static la probabilidad de que cada documento sea devuelto será static así como. Esto es un problema, ya que algunos valores pueden tener probabilidades injustamente bajas o altas en función de los valores aleatorios iniciales que obtuvieron. En muchos casos de uso, esto está bien, pero en algunos, es posible que desee aumentar la aleatoriedad a largo plazo para tener una probabilidad más uniforme de devolver cualquier documento.

Tenga en cuenta que los documentos insertados terminarán entretejidos en el medio, cambiando gradualmente las probabilidades, al igual que la eliminación de documentos. Si la tasa de inserción / eliminación es demasiado pequeña dada la cantidad de documentos, existen algunas estrategias para abordar esto.

Multi-aleatorio

En lugar de preocuparse por resembrar, siempre puede crear múltiples índices aleatorios por documento y luego seleccionar aleatoriamente uno de esos índices cada vez. Por ejemplo, haz que el campo random ser un mapa con los subcampos 1 a 3:

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

Ahora consultará contra random.1, random.2, random.3 aleatoriamente, creando una mayor dispersión de la aleatoriedad. Básicamente, esto intercambia un mayor almacenamiento para ahorrar un mayor cálculo (escritura de documentos) de tener que reiniciar.

Reseed en escrituras

Cada vez que actualice un documento, vuelva a generar los valores aleatorios del random campo. Esto moverá el documento en el índice aleatorio.

Resembrar en lecturas

Si los valores aleatorios generados no se distribuyen uniformemente (son aleatorios, por lo que es lo esperado), entonces el mismo documento podría seleccionarse una cantidad de tiempo inadecuada. Esto se contrarresta fácilmente actualizando el documento seleccionado aleatoriamente con nuevos valores aleatorios después de su lectura.

Dado que las escrituras son más caras y pueden ser un punto de acceso, puede optar por actualizar solo al leer un subconjunto del tiempo (por ejemplo, if random(0,100) === 0) update;).

Publicando esto para ayudar a cualquiera que tenga este problema en el futuro.

Si está utilizando Auto ID, puede generar un nuevo Auto ID y consultar el Auto ID más cercano como se menciona en la Respuesta de Dan McGrath.

Recientemente creé una API de citas aleatorias y necesitaba obtener citas aleatorias de una colección de Firestore.
Así es como resolví ese problema:

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

los key a la consulta es esta:

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

Y volver a llamarlo con la operación revertida si no se encuentran documentos.

¡Espero que esto ayude!
Si está interesado, puede encontrar esta parte específica de mi API en GitHub

Acabo de hacer que esto funcione en Angular 7 + RxJS, por lo que lo comparto aquí con personas que quieran un ejemplo.

Usé la respuesta de @Dan McGrath y elegí estas opciones: Versión de entero aleatorio + Enjuague y repetición para varios números. También utilicé lo que se explica en este artículo: RxJS, ¿dónde está el operador If-Else? para hacer declaraciones if / else a nivel de transmisión (solo si alguno de ustedes necesita una introducción a eso).

También tenga en cuenta que utilicé angularfire2 para una fácil integración de Firebase en Angular.

Aquí está el código:

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 
    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) => 
        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 

  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;
  


valoraciones y reseñas

Recuerda que puedes dar recomendación a este escrito si te ayudó.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *