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.

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:

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:
- Todos los campos que se usarán en los filtros de igualdad (en cualquier orden)
- Todos los campos por los que se ordenará (en el mismo orden)
- 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?
- Comienza a explorar la documentación de referencia de las funciones y las etapas.