Menu
CoddyTech

Pipelines de agregação

Parte da seção Bancos de dados com MongoDB do Journey de Node.js da Coddy. Lição 24 de 29.

Uma lista dos mais bem avaliados precisa de médias, de um número mínimo de avaliações, de uma ordenação e de um corte. Cada um desses elementos é uma etapa.

$match mantém os documentos que passam por um filtro, como find. $avg calcula a média de um campo dentro de $group. As etapas são executadas na ordem em que você as escreve, e cada uma só vê o que a etapa anterior passou adiante. Um $match antes de $group filtra os documentos armazenados. Um $match depois de $group filtra os grupos pelos campos criados pelo grupo, como em { $match: { orders: { $gte: 3 } } }. Um $group pode criar vários campos de uma só vez, como spent: { $sum: '$total' } e orders: { $sum: 1 }.

await orders.aggregate([
    { $match: { status: 'paid' } },
    { $group: { _id: '$customer', spent: { $sum: '$total' } } },
    { $sort: { spent: -1 } },
    { $limit: 5 },
]).toArray();

$sort e $limit recebem os mesmos argumentos que em um cursor, mas um pipeline os executa na ordem em que você os escreve. Um $limit antes de $sort mantém quaisquer cinco documentos e ordena apenas esses.

challenge icon

Desafio

Fácil

Esta etapa abre o projeto da API de estante preparado para o curso, em vez dos arquivos do seu projeto de Agrupamento com $group (incluindo server.js). Seu trabalho anterior continua na própria lição. Leia os arquivos fornecidos antes de editar server.js. Esta cópia contém as rotas da estante e o resumo da contagem por gênero.

A estante quer uma lista dos “mais bem avaliados”: os três livros com as maiores médias de estrelas. Um livro precisa ter pelo menos duas avaliações para entrar na lista, então uma única avaliação sortuda de 5 estrelas não o coloca no topo.

  1. Em server.js, adicione uma rota GET /stats/top. Ela executa uma agregação na coleção ratings, não em books.
  2. Agrupe as avaliações por bookId. Cada grupo recebe três campos: _id, que é o id do livro; avg, a média das stars; e count, o número de avaliações que ele tem. Uma etapa $group pode calcular tanto avg quanto count.
  3. Mantenha apenas os grupos cujo count seja 2 ou mais. Essa regra se aplica ao grupo de um livro, não a uma avaliação, então essa etapa vem depois do agrupamento.
  4. Ordene os grupos por avg, da maior para a menor, e mantenha os 3 primeiros.
  5. Responda com o status 200 e o array. Cada item tem exatamente estes três campos:
    [
      { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 },
      { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 }
    ]
    _id é o _id do livro, para que um cliente possa buscar o livro com GET /books/ mais esse id. Quando menos de três livros tiverem duas avaliações, o array será menor. Experimente: cada execução de Run esvazia ratings, então envie estas requisições depois da sua última execução. Envie GET /books e copie o _id de Dune, Kindred e Emma. Envie PUT /books/<Dune's _id>/ratings com { "user": "ada", "stars": 5 }, depois envie novamente com { "user": "bo", "stars": 4 }. Avalie Kindred da mesma forma, com 3 estrelas de ada e 3 de bo. Avalie Emma uma vez, com { "user": "ada", "stars": 5 }. Envie GET /stats/top: você receberá Dune (avg 4.5, count 2) e, em seguida, Kindred (avg 3, count 2). Emma não aparece, porque tem apenas uma avaliação. Se Emma aparecer primeiro, seu filtro de contagem está sendo executado antes do agrupamento, ou não está sendo executado.

Experimente você mesmo

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

// Todo erro retorna { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// O :id como ObjectId, ou null após retornar 400 para um id invá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');
});
quiz iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Bancos de dados com MongoDB

Pratique por conta própria: Playground de Express