Canalizaciones de agregación
Parte de la sección Bases de datos con MongoDB del Journey de Node.js de Coddy. Lección 24 de 29.
Una lista de los «mejor valorados» necesita promedios, un número mínimo de valoraciones, un orden y un límite. Cada uno de ellos es una etapa.
$match conserva los documentos que cumplen un filtro, como find. $avg calcula el promedio de un campo dentro de $group. Las etapas se ejecutan en el orden en que las escribes, y cada una solo ve lo que le pasó la etapa anterior. Un $match antes de $group filtra los documentos almacenados. Un $match después de $group filtra los grupos usando los campos que creó el grupo, como en { $match: { orders: { $gte: 3 } } }. Un $group puede crear varios campos a la vez, como spent: { $sum: '$total' } y orders: { $sum: 1 }.
await orders.aggregate([
{ $match: { status: 'paid' } },
{ $group: { _id: '$customer', spent: { $sum: '$total' } } },
{ $sort: { spent: -1 } },
{ $limit: 5 },
]).toArray();$sort y $limit aceptan los mismos argumentos que en un cursor, pero una canalización los ejecuta en el orden en que los escribes. Un $limit antes de $sort conserva cinco elementos cualesquiera y solo ordena esos.
Desafío
FácilEste paso abre el proyecto de API de biblioteca preparado para el curso, en lugar de los archivos de tu proyecto de Agrupar con $group (incluido server.js). Tu trabajo anterior sigue en su propia lección. Lee los archivos proporcionados antes de editar server.js. Esta copia contiene las rutas de la biblioteca y el resumen del recuento por género.
La biblioteca quiere una lista de los «mejor valorados»: los tres libros con el promedio de estrellas más alto. Un libro necesita al menos dos valoraciones para aparecer en la lista, así que una sola valoración afortunada de 5 estrellas no lo coloca en los primeros puestos.
- En
server.js, añade una rutaGET /stats/top. Ejecuta una agregación en la colecciónratings, no enbooks. - Agrupa las valoraciones por
bookId. Cada grupo tiene tres campos:_id, que es el id del libro;avg, el promedio de susstars; ycount, el número de valoraciones que tiene. Una etapa$grouppuede calcular tantoavgcomocount. - Conserva solo los grupos cuyo
countsea 2 o más. Esta regla se aplica al grupo de un libro, no a una sola valoración, así que esta etapa va después de la agrupación. - Ordena los grupos por
avg, de mayor a menor, y conserva los primeros 3. - Responde con el estado 200 y el arreglo. Cada elemento tiene exactamente estos tres campos:
[ { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 }, { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 } ]_ides el_iddel libro, así que un cliente puede obtener el libro conGET /books/seguido de ese id. Si menos de tres libros tienen dos valoraciones, el arreglo será más corto. Pruébalo: cada vez que pulses «Ejecutar», se vacíaratings, así que envía estas solicitudes después de tu última ejecución. EnvíaGET /booksy copia el_idde Dune, Kindred y Emma. EnvíaPUT /books/<Dune's _id>/ratingscon{ "user": "ada", "stars": 5 }y, luego, otra vez con{ "user": "bo", "stars": 4 }. Valora Kindred de la misma manera, con 3 estrellas deaday 3 debo. Valora Emma una vez, con{ "user": "ada", "stars": 5 }. EnvíaGET /stats/top: obtendrás Dune (avg4.5,count2) y, después, Kindred (avg3,count2). Emma no aparece porque solo tiene una valoración. Si Emma aparece primero, el filtro por recuento se ejecuta antes de la agrupación o no se ejecuta en absoluto.
Pruébalo tú mismo
import express from 'express';
import { MongoClient, ObjectId } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('shelf');
const books = db.collection('books');
const ratings = db.collection('ratings');
await books.createIndex({ isbn: 1 }, { unique: true });
await books.insertMany([
{ title: 'Dune', author: 'Frank Herbert', year: 1965, isbn: '9780441013593', genre: 'scifi' },
{ title: 'Kindred', author: 'Octavia E. Butler', year: 1979, isbn: '9780807083697', genre: 'scifi' },
{ title: 'Emma', author: 'Jane Austen', year: 1815, isbn: '9780141439587', genre: 'classic' },
]);
const app = express();
app.use(express.json());
// Cada error responde con { error: message }.
function sendError(res, status, message) {
res.status(status).json({ error: message });
}
// El :id como ObjectId, o null tras responder con 400 si el id no es válido.
function parseId(req, res) {
if (ObjectId.isValid(req.params.id)) {
return new ObjectId(req.params.id);
}
sendError(res, 400, 'Not a valid id');
return null;
}
app.get('/', (req, res) => {
res.send('Bookshelf API');
});
app.get('/books', async (req, res) => {
const filter = {};
if (req.query.author) {
filter.author = req.query.author;
}
if (req.query.from || req.query.to) {
filter.year = {};
if (req.query.from) {
filter.year.$gte = Number(req.query.from);
}
if (req.query.to) {
filter.year.$lte = Number(req.query.to);
}
}
const list = await books.find(filter).toArray();
res.json(list);
});
app.post('/books', async (req, res) => {
const book = req.body;
try {
await books.insertOne(book);
} catch (err) {
if (err.code === 11000) {
return sendError(res, 409, 'That ISBN is already on the shelf');
}
throw err;
}
res.status(201).json(book);
});
app.get('/books/newest', async (req, res) => {
const n = Number(req.query.n) || 3;
const list = await books
.find({}, { projection: { title: 1, year: 1, _id: 0 } })
.sort({ year: -1 })
.limit(n)
.toArray();
res.json(list);
});
app.get('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const book = await books.findOne({ _id: id });
if (!book) {
return sendError(res, 404, 'No such book');
}
res.json(book);
});
app.patch('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $set: req.body });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/copies', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $inc: { copies: 1 } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.post('/books/:id/tags', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.updateOne({ _id: id }, { $push: { tags: req.body.tag } });
if (result.matchedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.json(await books.findOne({ _id: id }));
});
app.delete('/books/:id', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const result = await books.deleteOne({ _id: id });
if (result.deletedCount === 0) {
return sendError(res, 404, 'No such book');
}
res.status(204).end();
});
app.put('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
const { user, stars } = req.body;
await ratings.updateOne({ bookId: id, user }, { $set: { stars } }, { upsert: true });
res.json({ user, stars });
});
app.get('/books/:id/ratings', async (req, res) => {
const id = parseId(req, res);
if (!id) return;
res.json(await ratings.find({ bookId: id }).toArray());
});
app.get('/stats', async (req, res) => {
const filter = req.query.genre ? { genre: req.query.genre } : {};
res.json({
books: await books.countDocuments(filter),
ratings: await ratings.countDocuments(),
});
});
app.get('/stats/genres', async (req, res) => {
const genres = await books
.aggregate([{ $group: { _id: '$genre', count: { $sum: 1 } } }])
.toArray();
res.json(genres);
});
app.listen(3000, () => {
console.log('Bookshelf is running on port 3000');
});
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Bases de datos con MongoDB
1Documentos
Un libro como documentoColecciones y bases de datosConexión a MongoDBLectura de documentos almacenadosInserción de documentosBúsqueda de un documentoBúsqueda por ObjectIdComparación de númerosFiltros de consultaSelección de los campos devueltosOrdenación de los resultados de consultaConservar los primeros resultadosProyección, ordenación y límite2Modificación de documentos
Actualización de documentosOperadores de actualización3Agregación
Contar documentosUna canalización de etapasAgrupar con $groupPromedios dentro de gruposFiltrar resultados de gruposCanalizaciones de agregaciónUnir con $lookupPractica por tu cuenta: Playground de Express