Firestore: Cómo solventar el límite de 10 elementos con queries 'IN' y 'array-contains-any'

Una de las grandes herramientas de Firebase es el famoso Firestore. Es un sistema de base de datos NoSQL de documentos construido tomando en cuenta la escalabilidad automática, gran rendimiento y facilidad en el desarrollo de aplicaciones. Por lo general, uno se adapta rápidamente a trabajar con esta base de datos y hacer los queries de forma sencilla. Todo va a depender de cómo diseñes tu base de datos en Firestore.

El problema

Entre todas las formas condicionales que ofrece Firestore para los queries, nos encontramos con un par que puede ser extremadamente útiles: ‘in’ y ‘array-contains-any’.

‘in’

La primera es relativamente sencilla. Buscará documentos por un campo cuyo valor coincida con los valores en un arreglo. Ejemplo:

Tienes una colección de etiquetas (’tags’). Cada etiqueta tiene sus respectivas entradas como ’nombre’ y ‘color’. El caso consiste en buscar las etiquetas que tengan ciertos colores. Los colores estarán almacenados en un arreglo y los usaremos para obtener los documentos necesarios de Firestore.

const colors: string[] = ['Azul','Verde','Amarillo','Blanco','Naranja','Rojo','Morado','Gris','negro','Cyan','Vinotinto','Rosado'];
let query = firestore.collection("tags").where("color", "in", colors);
const tags = await query.get();

Esto sería lo más apropiado, pero tenemos un problema. Cuando se ejecute dicha instrucción, tendremos el siguiente error:

[Unhandled promise rejection: FirebaseError: Invalid Query. ‘in’ filters support a maximum of 10 elements in the value array.]

Nota: Dependiendo del lenguaje o framework que uses, puede que el mensaje de error varíe un poco pero la idea principal del mensaje es la misma.

Básicamente lo que nos dice es que no podemos usar la condición ‘in’ con un array que tenga más de 10 valores. Si prestamos un poco más de atención al código, vemos que existen 12 colores en el arreglo ‘colors’. El error también aparecerá cuando uses ‘array-contains-any’.

‘array-contains-any’

‘array-contains-any’ buscará todos los documentos cuyos arreglos contenga alguno de los valores especificados en un arreglo. Ejemplo:

Tienes una colección de camisas (‘shirts’) que posee una entrada llamada ‘colors’. Esta entrada es un arreglo de colores representando los colores disponibles para esa camisa.

const colors: string[] = ['Azul','Verde'];
let query = firestore.collection("shirts").where("colors", "array-contains-any", colors);
const shirts = await query.get();

Nos traerá los documentos de las camisas que estén disponibles en color Azul y Verde. Ahora, si tratamos de hacer la búsqueda pero con más de 10 elementos en el arreglo, ejemplo:

const colors: string[] = ['Azul','Verde','Amarillo','Blanco','Naranja','Rojo','Morado','Gris','negro','Cyan','Vinotinto','Rosado'];
let query = firestore.collection("shirts").where("colors", "array-contains-any", colors);
const shirts = await query.get();

Tendremos el mismo error:

[Unhandled promise rejection: FirebaseError: Invalid Query. ‘array-contains-any’ filters support a maximum of 10 elements in the value array.]

Entonces, ¿Cómo lo manejamos? Para ello, necesitamos ajustar la mentalidad y lógica que usamos al trabajar con Firebase.

Dejar a un lado la mentalidad ‘SQL’

Si eres como yo, probablemente hayas trabajado bastante con bases de datos SQL donde puedes hacer queries extremadamente largos, complejos, prácticos e inútiles también. Puedes hacer una sentencia que llegue a tener 100 líneas de caracteres y aún obtener resultados. Cuando empiezas a trabajar con Firebase y Firestore, dejar esa costumbre y mentalidad puede resultar desafiante.

Mis primeras impresiones eran cosas como: “Pero en MySQL puedo hacer esto con una sentencia” o “¿Cómo es que Firestore no puede hacer un query si el arreglo de búsqueda tiene más de 10 elementos?”. Al leer la documentación oficial de Firebase, veía lo siguiente:

Limitaciones

Ten en cuenta las siguientes limitaciones de in, not-in, y array-contains-any:

  • ‘in’, ’not-in’ y ‘array-contains-any’ admiten hasta 10 valores de comparación.
  • Puedes usar como máximo una cláusula ‘array-contains’ por consulta. No puedes combinar ‘array-contains’ con ‘array-contains-any’.
  • Puedes usar como máximo una cláusula ‘in’, ’not-in’ o ‘array-contains-any’ por consulta. No puedes combinar estos operadores en la misma consulta.
  • No puedes combinar ’not-in’ con no es igual a !=.
  • No puedes ordenar tu consulta por un campo incluido en una cláusula de igualdad (==) o ‘in’.

Entiendo que como Firestore está disponible de forma gratuita a cualquier usuario con una cuenta Google y no tienes que pagar absolutamente nada por el plan gratuito de Firebase, Google busca de eliminar los esfuerzos innecesarios de Firestore para que pueda ser efectivo y rápido para todas las aplicaciones. La rapidéz es una de las características que Google busca con Firebase. Nadie quiere esperar más de 10 segundos por una consulta en BD.

Solución

Mi recomendación principal va a ser que revises la lógica de tu código y tu estructura de datos en Firestore. Si, por ejemplo, estás buscando de realizar una búsqueda de etiquetas con el condicional ‘in’ y un arreglo de 12 colores, tal vez sea buena idea preguntarte:

¿De verdad necesitas incluir 12 colores en tu búsqueda?

Las posibilidades donde hacer una búsqueda de este estilo sea la única opción son extremadamente bajas. Posiblemente no necesites buscar por los doce colores. Por ello, una solución práctica puede ser:

  1. Obtener todas las etiquetas. (Será una búsqueda muy rápida y te traerá de una vez toda la colección de etiquetas)
  2. Guardar las etiquetas en un arreglo procesando la data de los documentos como mejor consideres.
  3. Filtrar las etiquetas en el código eliminando las que no necesites. Teniendo ya la data descargada, procesarla es mucho más rápido. Recordemos que Javascript, Typescript, Dart, etc.; pueden realizar estas tareas en milésimas de segundos.

En caso de necesitar buscar por arreglos con ‘in’ o ‘array-contains-any’

Si llegaste hasta acá y todavía necesitas una mejor solución al problema usando ‘in’ con un arreglo mayor a 10 elementos en el query, entonces podrías intentar lo siguiente:

const colors: string[] = ['Azul','Verde','Amarillo','Blanco','Naranja','Rojo','Morado','Gris','negro','Cyan','Vinotinto','Rosado'];

export async function getTagsByColors(colors) {
  if (!colors || !colors.length ) return []; // No ejecutes la función si el arreglo está vacío o no existe.

  const tagsCollectionPath = db.collection('tags'); 
  const batches = [];

  while (colors.length) {
    const batch = colors.splice(0, 10); // Firebase limita la búsqueda a 10 elementos en el arreglo.

    // Añadimos el batch a una cola
    batches.push(
      tagsCollectionPath
        .where(
          'color',
          'in',
          [...batch]
        )
        .get()
        .then(results => results.docs.map(result => ({ ...result.data() }) ))
    )
  }

  // Una vez lista la cola, la retornamos con un 'Promise.all'
  return Promise.all(batches)
    .then(content => content.flat());
}

Si bien este código puede funcionar para esa necesidad puntual en Firestore, no es la mejor práctica. Firebase tiene una cuota gratuita de consultas a Firestore, por lo que aplicar una buena práctica a la solicitud y manejo de datos con Firestore puede ahorrarte muchos dolores de cabeza en el futuro.