Comienza a usar la operación de Pipelines de Firestore

Segundo plano

Las consultas de canalización son una nueva interfaz de consultas para Firestore. Proporciona funciones de consulta avanzadas, incluidas expresiones complejas. También se agregó compatibilidad con muchas funciones nuevas, como min, max, substring, regex_match y array_contains_all. Con las consultas de canalización, la creación de índices también es completamente opcional, lo que agiliza el proceso de desarrollo de consultas nuevas. Las consultas de Pipeline también quitan muchas limitaciones en la forma de la consulta, lo que te permite especificar consultas in o or grandes.

Primeros pasos

Para instalar y, luego, inicializar los SDKs de cliente, consulta las instrucciones de la guía de inicio.

Sintaxis

En las siguientes secciones, se proporciona una descripción general de la sintaxis de las consultas de canalización.

Conceptos

Una diferencia notable con las consultas de canalización es la introducción del ordenamiento explícito de "etapas". Esto permite expresar consultas más complejas. Sin embargo, es una desviación notable de la interfaz de consultas existente, en la que el orden de las etapas estaba implícito. Considera el siguiente ejemplo de consulta de canalización:

Web

const pipeline = db.pipeline()
  // Step 1: Start a query with collection scope
  .collection("cities")
  // Step 2: Filter the collection
  .where(field("population").greaterThan(100000))
  // Step 3: Sort the remaining documents
  .sort(field("name").ascending())
  // Step 4: Return the top 10. Note applying the limit earlier in the
  // pipeline would have unintentional results.
  .limit(10);
Swift
let pipeline = db.pipeline()
  // Step 1: Start a query with collection scope
  .collection("cities")
  // Step 2: Filter the collection
  .where(Field("population").greaterThan(100000))
  // Step 3: Sort the remaining documents
  .sort([Field("name").ascending()])
  // Step 4: Return the top 10. Note applying the limit earlier in the pipeline would have
  // unintentional results.
  .limit(10)

Kotlin

val pipeline = db.pipeline()
    // Step 1: Start a query with collection scope
    .collection("cities")
    // Step 2: Filter the collection
    .where(field("population").greaterThan(100000))
    // Step 3: Sort the remaining documents
    .sort(field("name").ascending())
    // Step 4: Return the top 10. Note applying the limit earlier in the pipeline would have
    // unintentional results.
    .limit(10)

Java

Pipeline pipeline = db.pipeline()
    // Step 1: Start a query with collection scope
    .collection("cities")
    // Step 2: Filter the collection
    .where(field("population").greaterThan(100000))
    // Step 3: Sort the remaining documents
    .sort(field("name").ascending())
    // Step 4: Return the top 10. Note applying the limit earlier in the pipeline would have
    // unintentional results.
    .limit(10);
Python
from google.cloud.firestore_v1.pipeline_expressions import Field

pipeline = (
    client.pipeline()
    .collection("cities")
    .where(Field.of("population").greater_than(100_000))
    .sort(Field.of("name").ascending())
    .limit(10)
)

Inicialización

Las consultas de canalización tienen una sintaxis muy familiar que proviene de las consultas de Cloud Firestore existentes. Para comenzar, inicializa una consulta escribiendo lo siguiente:

Web

const { getFirestore } = require("firebase/firestore");
const { execute } = require("firebase/firestore/pipelines");
const database = getFirestore(app, "enterprise");
const pipeline = database.pipeline();
Swift
let firestore = Firestore.firestore(database: "enterprise")
let pipeline = firestore.pipeline()

Kotlin

val firestore = Firebase.firestore("enterprise")
val pipeline = firestore.pipeline()

Java

FirebaseFirestore firestore = FirebaseFirestore.getInstance("enterprise");
PipelineSource pipeline = firestore.pipeline();
Python
firestore_client = firestore.client(default_app, "your-new-enterprise-database")
pipeline = firestore_client.pipeline()

Estructura

Hay algunos términos que es importante comprender cuando se crean consultas de canalización: etapas, expresiones y funciones.

Ejemplo en el que se muestran etapas y expresiones en una consulta

Etapas: una canalización puede constar de una o más etapas. Lógicamente, representan la serie de pasos (o etapas) que se siguen para ejecutar la consulta. Nota: En la práctica, las etapas pueden ejecutarse fuera de orden para mejorar el rendimiento. Sin embargo, esto no modifica la intención ni la corrección de la consulta.

Expresiones: Las etapas suelen aceptar una expresión que te permite expresar consultas más complejas. La expresión puede ser simple y constar de una sola función, como eq("a", 1). También puedes usar expresiones más complejas anidando expresiones como and(eq("a", 1), eq("b", 2))..

Referencias de campo y constantes

Las consultas de canalización admiten expresiones complejas. Por lo tanto, puede ser necesario diferenciar si un valor representa un campo o una constante. Considera el siguiente ejemplo:

Web

const pipeline = db.pipeline()
  .collection("cities")
  .where(field("name").equal(constant("Toronto")));
Swift
let pipeline = db.pipeline()
  .collection("cities")
  .where(Field("name").equal(Constant("Toronto")))

Kotlin

val pipeline = db.pipeline()
    .collection("cities")
    .where(field("name").equal(constant("Toronto")))

Java

Pipeline pipeline = db.pipeline()
    .collection("cities")
    .where(field("name").equal(constant("Toronto")));
Python
from google.cloud.firestore_v1.pipeline_expressions import Field, Constant

pipeline = (
    client.pipeline()
    .collection("cities")
    .where(Field.of("name").equal(Constant.of("Toronto")))
)

Etapas

Etapas de entrada

La etapa de entrada representa la primera etapa de una consulta. Define el conjunto inicial de documentos sobre los que realizas la consulta. En el caso de las consultas de canalización, esto es muy similar a las consultas existentes, en las que la mayoría de las consultas comienzan con una etapa collection(...) o collection_group(...). Las dos nuevas etapas de entrada son database() y documents(...), en las que database() permite devolver todos los documentos de la base de datos, mientras que documents(...) actúa de forma idéntica a una lectura por lotes.

Web

let results;

// Return all restaurants in San Francisco
results = await execute(db.pipeline().collection("cities/sf/restaurants"));

// Return all restaurants
results = await execute(db.pipeline().collectionGroup("restaurants"));

// Return all documents across all collections in the database (the entire database)
results = await execute(db.pipeline().database());

// Batch read of 3 documents
results = await execute(db.pipeline().documents([
  doc(db, "cities", "SF"),
  doc(db, "cities", "DC"),
  doc(db, "cities", "NY")
]));
Swift
var results: Pipeline.Snapshot

// Return all restaurants in San Francisco
results = try await db.pipeline().collection("cities/sf/restaurants").execute()

// Return all restaurants
results = try await db.pipeline().collectionGroup("restaurants").execute()

// Return all documents across all collections in the database (the entire database)
results = try await db.pipeline().database().execute()

// Batch read of 3 documents
results = try await db.pipeline().documents([
  db.collection("cities").document("SF"),
  db.collection("cities").document("DC"),
  db.collection("cities").document("NY")
]).execute()

Kotlin

var results: Task<Pipeline.Snapshot>

// Return all restaurants in San Francisco
results = db.pipeline().collection("cities/sf/restaurants").execute()

// Return all restaurants
results = db.pipeline().collectionGroup("restaurants").execute()

// Return all documents across all collections in the database (the entire database)
results = db.pipeline().database().execute()

// Batch read of 3 documents
results = db.pipeline().documents(
    db.collection("cities").document("SF"),
    db.collection("cities").document("DC"),
    db.collection("cities").document("NY")
).execute()

Java

Task<Pipeline.Snapshot> results;

// Return all restaurants in San Francisco
results = db.pipeline().collection("cities/sf/restaurants").execute();

// Return all restaurants
results = db.pipeline().collectionGroup("restaurants").execute();

// Return all documents across all collections in the database (the entire database)
results = db.pipeline().database().execute();

// Batch read of 3 documents
results = db.pipeline().documents(
    db.collection("cities").document("SF"),
    db.collection("cities").document("DC"),
    db.collection("cities").document("NY")
).execute();
Python
# Return all restaurants in San Francisco
results = client.pipeline().collection("cities/sf/restaurants").execute()

# Return all restaurants
results = client.pipeline().collection_group("restaurants").execute()

# Return all documents across all collections in the database (the entire database)
results = client.pipeline().database().execute()

# Batch read of 3 documents
results = (
    client.pipeline()
    .documents(
        client.collection("cities").document("SF"),
        client.collection("cities").document("DC"),
        client.collection("cities").document("NY"),
    )
    .execute()
)

Al igual que con todas las demás etapas, el orden de los resultados de estas etapas de entrada no es estable. Siempre se debe agregar un operador sort(...) si se desea un orden específico.

Etapa Where

La etapa where(...) actúa como una operación de filtro tradicional sobre los documentos generados en la etapa anterior y, en su mayoría, refleja la sintaxis "where" existente para las consultas existentes. Cualquier documento en el que una expresión determinada se evalúe como un valor que no sea true se filtra de los documentos devueltos.

Se pueden encadenar varias sentencias where(...) y actuar como una expresión and(...). Por ejemplo, las dos consultas siguientes son lógicamente equivalentes y se pueden usar indistintamente.

Web

let results;

results = await execute(db.pipeline().collection("books")
  .where(field("rating").equal(5))
  .where(field("published").lessThan(1900))
);

results = await execute(db.pipeline().collection("books")
  .where(and(field("rating").equal(5), field("published").lessThan(1900)))
);
Swift
var results: Pipeline.Snapshot

results = try await db.pipeline().collection("books")
  .where(Field("rating").equal(5))
  .where(Field("published").lessThan(1900))
  .execute()

results = try await db.pipeline().collection("books")
  .where(Field("rating").equal(5) && Field("published").lessThan(1900))
  .execute()

Kotlin

var results: Task<Pipeline.Snapshot>

results = db.pipeline().collection("books")
    .where(field("rating").equal(5))
    .where(field("published").lessThan(1900))
    .execute()

results = db.pipeline().collection("books")
    .where(Expression.and(field("rating").equal(5),
      field("published").lessThan(1900)))
    .execute()

Java

Task<Pipeline.Snapshot> results;

results = db.pipeline().collection("books")
    .where(field("rating").equal(5))
    .where(field("published").lessThan(1900))
    .execute();

results = db.pipeline().collection("books")
    .where(Expression.and(
        field("rating").equal(5),
        field("published").lessThan(1900)
    ))
    .execute();
Python
from google.cloud.firestore_v1.pipeline_expressions import And, Field

results = (
    client.pipeline()
    .collection("books")
    .where(Field.of("rating").equal(5))
    .where(Field.of("published").less_than(1900))
    .execute()
)

results = (
    client.pipeline()
    .collection("books")
    .where(And(Field.of("rating").equal(5), Field.of("published").less_than(1900)))
    .execute()
)

Selecciona, agrega y quita campos

Los comandos select(...), add_fields(...) y remove_fields(...) te permiten modificar los campos que se devuelven en una etapa anterior. Estos tres tipos se conocen generalmente como etapas de proyección.

Los comandos select(...) y add_fields(...) te permiten especificar el resultado de una expresión en un nombre de campo proporcionado por el usuario. Una expresión que genera un error dará como resultado un valor null. El select(...) solo devolverá los documentos con los nombres de campo especificados, mientras que add_fields(...) extiende el esquema de la etapa anterior (y puede reemplazar valores con nombres de campo idénticos).

Con remove_fields(...), se puede especificar un conjunto de campos para quitar de la etapa anterior. Especificar nombres de campos que no existen no generan ninguna operación.

Consulta la sección Restringe los campos que se devolveránque aparece a continuación, pero, en general, usar una etapa de este tipo para restringir el resultado solo a los campos necesarios en el cliente ayuda a reducir el costo y la latencia de la mayoría de las consultas.

Agregación o distinción

La etapa aggregate(...) te permite realizar una serie de agregaciones en los documentos de entrada. De forma predeterminada, todos los documentos se agregan juntos, pero se puede proporcionar un argumento grouping opcional, lo que permite que los documentos de entrada se agreguen en diferentes buckets.

Web

const results = await execute(db.pipeline()
  .collection("books")
  .aggregate(
    field("rating").average().as("avg_rating")
  )
  .distinct(field("genre"))
);
Swift
let results = try await db.pipeline()
  .collection("books")
  .aggregate([
    Field("rating").average().as("avg_rating")
  ], groups: [
    Field("genre")
  ])
  .execute()

Kotlin

val results = db.pipeline()
    .collection("books")
    .aggregate(
        AggregateStage
            .withAccumulators(AggregateFunction.average("rating").alias("avg_rating"))
            .withGroups(field("genre"))
    )
    .execute()

Java

Task<Pipeline.Snapshot> results = db.pipeline()
    .collection("books")
    .aggregate(AggregateStage
        .withAccumulators(
            AggregateFunction.average("rating").alias("avg_rating"))
        .withGroups(field("genre")))
    .execute();
Python
from google.cloud.firestore_v1.pipeline_expressions import Field

results = (
    client.pipeline()
    .collection("books")
    .aggregate(
        Field.of("rating").average().as_("avg_rating"), groups=[Field.of("genre")]
    )
    .execute()
)

Cuando no se especifica groupings, esta etapa solo producirá un documento. De lo contrario, se generará un documento para cada combinación única de valores groupings.

La etapa distinct(...) es un operador de agregación simplificado que permite generar solo el valor groupings único sin ningún acumulador. En todos los demás aspectos, se comporta de manera idéntica a la de aggregate(...). A continuación, se muestra un ejemplo:

Web

const results = await execute(db.pipeline()
  .collection("books")
  .distinct(
    field("author").toUpper().as("author"),
    field("genre")
  )
);
Swift
let results = try await db.pipeline()
  .collection("books")
  .distinct([
    Field("author").toUpper().as("author"),
    Field("genre")
  ])
  .execute()

Kotlin

val results = db.pipeline()
    .collection("books")
    .distinct(
        field("author").toUpper().alias("author"),
        field("genre")
    )
    .execute()

Java

Task<Pipeline.Snapshot> results = db.pipeline()
    .collection("books")
    .distinct(
        field("author").toUpper().alias("author"),
        field("genre")
    )
    .execute();
Python
from google.cloud.firestore_v1.pipeline_expressions import Field

results = (
    client.pipeline()
    .collection("books")
    .distinct(Field.of("author").to_upper().as_("author"), "genre")
    .execute()
)

Funciones

Las funciones son un componente básico para crear expresiones y consultas complejas. Para obtener una lista completa de las funciones con ejemplos, consulta la Referencia de las funciones. A modo de recordatorio, considera la estructura de una consulta típica:

Ejemplo en el que se muestran las etapas y las funciones en una consulta

Muchas etapas aceptan expresiones que contienen una o más funciones. El uso de funciones más común se encontrará en las etapas where(...) y select(...). Existen dos tipos principales de funciones que debes conocer:

Web

let results;

// Type 1: Scalar (for use in non-aggregation stages)
// Example: Return the min store price for each book.
results = await execute(db.pipeline().collection("books")
  .select(field("current").logicalMinimum(field("updated")).as("price_min"))
);

// Type 2: Aggregation (for use in aggregate stages)
// Example: Return the min price of all books.
results = await execute(db.pipeline().collection("books")
  .aggregate(field("price").minimum().as("min_price"))
);
Swift
var results: Pipeline.Snapshot

// Type 1: Scalar (for use in non-aggregation stages)
// Example: Return the min store price for each book.
results = try await db.pipeline().collection("books")
  .select([
    Field("current").logicalMinimum(["updated"]).as("price_min")
  ])
  .execute()

// Type 2: Aggregation (for use in aggregate stages)
// Example: Return the min price of all books.
results = try await db.pipeline().collection("books")
  .aggregate([Field("price").minimum().as("min_price")])
  .execute()

Kotlin

var results: Task<Pipeline.Snapshot>

// Type 1: Scalar (for use in non-aggregation stages)
// Example: Return the min store price for each book.
results = db.pipeline().collection("books")
    .select(
        field("current").logicalMinimum("updated").alias("price_min")
    )
    .execute()

// Type 2: Aggregation (for use in aggregate stages)
// Example: Return the min price of all books.
results = db.pipeline().collection("books")
    .aggregate(AggregateFunction.minimum("price").alias("min_price"))
    .execute()

Java

Task<Pipeline.Snapshot> results;

// Type 1: Scalar (for use in non-aggregation stages)
// Example: Return the min store price for each book.
results = db.pipeline().collection("books")
    .select(
        field("current").logicalMinimum("updated").alias("price_min")
    )
    .execute();

// Type 2: Aggregation (for use in aggregate stages)
// Example: Return the min price of all books.
results = db.pipeline().collection("books")
    .aggregate(AggregateFunction.minimum("price").alias("min_price"))
    .execute();
Python
from google.cloud.firestore_v1.pipeline_expressions import Field

# Type 1: Scalar (for use in non-aggregation stages)
# Example: Return the min store price for each book.
results = (
    client.pipeline()
    .collection("books")
    .select(
        Field.of("current").logical_minimum(Field.of("updated")).as_("price_min")
    )
    .execute()
)

# Type 2: Aggregation (for use in aggregate stages)
# Example: Return the min price of all books.
results = (
    client.pipeline()
    .collection("books")
    .aggregate(Field.of("price").minimum().as_("min_price"))
    .execute()
)

Límites

En general, la edición Enterprise no impone límites en la forma de la consulta. En otras palabras, no estás limitado a una pequeña cantidad de valores en una consulta IN o una OR. En cambio, hay dos límites principales que debes tener en cuenta:

  • Plazo: 60 segundos (igual que en la edición Standard).
  • Uso de memoria: Límite de 128 MiB en la cantidad de datos materializados durante la ejecución de la consulta.

Errores

Es posible que encuentres consultas con errores por varios motivos. Aquí tienes un vínculo a los errores comunes y la acción asociada que puedes realizar:

Código de error Acción
DEADLINE_EXCEEDED La consulta que ejecutas supera el plazo de 60 segundos y requiere optimización adicional. Consulta la sección de rendimiento para obtener sugerencias. Si no puedes determinar la causa raíz del problema, comunícate con el equipo.
RESOURCE_EXHAUSTED La consulta que ejecutas supera los límites de memoria y requiere optimización adicional. Consulta la sección de rendimiento para obtener sugerencias. Si no puedes determinar la causa raíz del problema, comunícate con el equipo.
INTERNAL Comunícate con el equipo para obtener asistencia.

Rendimiento

A diferencia de las consultas existentes, las consultas de canalización no requieren que siempre haya un índice. Esto significa que una consulta puede mostrar una latencia más alta en comparación con las consultas existentes que habrían fallado de inmediato con un error de índice faltante FAILED_PRECONDITION. Para mejorar el rendimiento de las consultas de canalización, puedes seguir algunos pasos.

Crea índices

Índice utilizado

La explicación de la consulta te permite identificar si tu consulta se publica a través de un índice o si recurre a una operación menos eficiente, como un análisis de tabla. Si tu consulta no se realiza por completo desde un índice, puedes crear uno siguiendo las instrucciones.

Crea índices

Puedes seguir la documentación existente sobre la administración de índices para crear índices. Antes de crear un índice, familiarízate con las prácticas recomendadas generales para los índices en Firestore. Para asegurarte de que tu consulta pueda aprovechar los índices, sigue las prácticas recomendadas para crear índices con campos en el siguiente orden:

  1. Todos los campos que se usarán en los filtros de igualdad (en cualquier orden)
  2. Todos los campos por los que se ordenará (en el mismo orden)
  3. Campos que se usarán en filtros de rango o desigualdad en orden descendente de selectividad de la restricción de consulta

Por ejemplo, en la siguiente consulta:

Web

const results = await execute(db.pipeline()
  .collection("books")
  .where(field("published").lessThan(1900))
  .where(field("genre").equal("Science Fiction"))
  .where(field("rating").greaterThan(4.3))
  .sort(field("published").descending())
);
Swift
let results = try await db.pipeline()
  .collection("books")
  .where(Field("published").lessThan(1900))
  .where(Field("genre").equal("Science Fiction"))
  .where(Field("rating").greaterThan(4.3))
  .sort([Field("published").descending()])
  .execute()

Kotlin

val results = db.pipeline()
    .collection("books")
    .where(field("published").lessThan(1900))
    .where(field("genre").equal("Science Fiction"))
    .where(field("rating").greaterThan(4.3))
    .sort(field("published").descending())
    .execute()

Java

Task<Pipeline.Snapshot> results = db.pipeline()
    .collection("books")
    .where(field("published").lessThan(1900))
    .where(field("genre").equal("Science Fiction"))
    .where(field("rating").greaterThan(4.3))
    .sort(field("published").descending())
    .execute();
Python
from google.cloud.firestore_v1.pipeline_expressions import Field

results = (
    client.pipeline()
    .collection("books")
    .where(Field.of("published").less_than(1900))
    .where(Field.of("genre").equal("Science Fiction"))
    .where(Field.of("rating").greater_than(4.3))
    .sort(Field.of("published").descending())
    .execute()
)

El índice recomendado es un índice de alcance de la colección en books para (genre [...], published DESC, avg_rating DESC)..

Densidad de indexación

Cloud Firestore admite índices dispersos y no dispersos. Para obtener más información, consulta Densidad del índice.

Consultas cubiertas y los índices secundarios

Firestore puede omitir la recuperación del documento completo y solo devolver los resultados del índice si todos los campos que se devuelven están presentes en un índice secundario. Esto suele generar una mejora significativa en la latencia (y el costo). Con la siguiente consulta de ejemplo:

Web

const results = await execute(db.pipeline()
  .collection("books")
  .where(field("category").like("%fantasy%"))
  .where(field("title").exists())
  .where(field("author").exists())
  .select(field("title"), field("author"))
);
Swift
let results = try await db.pipeline()
  .collection("books")
  .where(Field("category").like("%fantasy%"))
  .where(Field("title").exists())
  .where(Field("author").exists())
  .select([Field("title"), Field("author")])
  .execute()

Kotlin

val results = db.pipeline()
    .collection("books")
    .where(field("category").like("%fantasy%"))
    .where(field("title").exists())
    .where(field("author").exists())
    .select(field("title"), field("author"))
    .execute()

Java

Task<Pipeline.Snapshot> results = db.pipeline()
    .collection("books")
    .where(field("category").like("%fantasy%"))
    .where(field("title").exists())
    .where(field("author").exists())
    .select(field("title"), field("author"))
    .execute();
Python
from google.cloud.firestore_v1.pipeline_expressions import Field

results = (
    client.pipeline()
    .collection("books")
    .where(Field.of("category").like("%fantasy%"))
    .where(Field.of("title").exists())
    .where(Field.of("author").exists())
    .select("title", "author")
    .execute()
)

Si la base de datos ya tiene un índice de alcance de la colección en books para (category [...], title [...], author [...]), puede evitar recuperar cualquier elemento de los documentos principales. En este caso, el orden en el índice no importa, y se usa [...] para indicarlo.

Restringe los campos que se devolverán

De forma predeterminada, una consulta de Firestore devuelve todos los campos de un documento, de forma similar a un SELECT * en los sistemas tradicionales. Sin embargo, si tu aplicación solo necesita un subconjunto de los campos, las etapas select(...) o restrict(...) se pueden usar para enviar este filtrado del servidor. Esto disminuirá el tamaño de la respuesta (lo que reducirá el costo de salida de la red) y mejorará la latencia.

Herramientas de solución de problemas

Explicación de consultas

La Explicación de consultas te permite obtener visibilidad en las métricas de ejecución y los detalles sobre los índices utilizados.

Métricas

Las consultas de canalización si están completamente integradas con las métricas de Firestore existentes.

Problemas y limitaciones conocidos

Índices especializados

Las consultas de canalización aún no admiten los tipos de índice existentes array-contains y vector. En lugar de rechazar esas consultas, Firestore intentará usar otros índices ascending y descending existentes. Se espera que, durante la versión preliminar privada, las consultas de canalización con esas expresiones array_contains o find_nearest sean más lentas que sus equivalentes existentes debido a esto.

Paginación

Durante la versión preliminar privada, no se admite la paginación sencilla de un conjunto de resultados. Esto se puede solucionar encadenando etapas where(...) y sort(...) equivalentes, como se muestra a continuación.

Web

// Existing pagination via `startAt()`
const q =
  query(collection(db, "cities"), orderBy("population"), startAt(1000000));

// Private preview workaround using pipelines
const pageSize = 2;
const pipeline = db.pipeline()
  .collection("cities")
  .select("name", "population", "__name__")
  .sort(field("population").descending(), field("__name__").ascending());

// Page 1 results
let snapshot = await execute(pipeline.limit(pageSize));

// End of page marker
const lastDoc = snapshot.results[snapshot.results.length - 1];

// Page 2 results
snapshot = await execute(
  pipeline
    .where(
      or(
        and(
          field("population").equal(lastDoc.get("population")),
          field("__name__").greaterThan(lastDoc.ref)
        ),
        field("population").lessThan(lastDoc.get("population"))
      )
    )
    .limit(pageSize)
);
Swift
// Existing pagination via `start(at:)`
let query = db.collection("cities").order(by: "population").start(at: [1000000])

// Private preview workaround using pipelines
let pipeline = db.pipeline()
  .collection("cities")
  .where(Field("population").greaterThanOrEqual(1000000))
  .sort([Field("population").descending()])

Kotlin

// Existing pagination via `startAt()`
val query = db.collection("cities").orderBy("population").startAt(1000000)

// Private preview workaround using pipelines
val pipeline = db.pipeline()
    .collection("cities")
    .where(field("population").greaterThanOrEqual(1000000))
    .sort(field("population").descending())

Java

// Existing pagination via `startAt()`
Query query = db.collection("cities").orderBy("population").startAt(1000000);

// Private preview workaround using pipelines
Pipeline pipeline = db.pipeline()
    .collection("cities")
    .where(field("population").greaterThanOrEqual(1000000))
    .sort(field("population").descending());
Python
from google.cloud.firestore_v1.pipeline_expressions import Field

# Existing pagination via `start_at()`
query = (
    client.collection("cities")
    .order_by("population")
    .start_at({"population": 1_000_000})
)

# Private preview workaround using pipelines
pipeline = (
    client.pipeline()
    .collection("cities")
    .where(Field.of("population").greater_than_or_equal(1_000_000))
    .sort(Field.of("population").descending())
)

Compatibilidad con el emulador

El emulador aún no admite las consultas de Pipeline.

Compatibilidad sin conexión y en tiempo real

Las consultas de Pipeline aún no tienen capacidades en tiempo real ni sin conexión.

¿Qué sigue?