Menu
CoddyTech

Pipeline di aggregazione

Fa parte della sezione Database con MongoDB del percorso Node.js di Coddy. Lezione 24 di 29.

Un elenco dei più votati richiede medie, un numero minimo di valutazioni, un ordinamento e un limite. Ognuno di questi è una fase.

$match mantiene i documenti che superano un filtro, come find. $avg calcola la media di un campo all’interno di $group. Le fasi vengono eseguite nell’ordine in cui le scrivi e ciascuna vede solo ciò che le è stato passato dalla fase precedente. Un $match prima di $group filtra i documenti memorizzati. Un $match dopo $group filtra i gruppi in base ai campi creati dal gruppo, come in { $match: { orders: { $gte: 3 } } }. Un $group può creare più campi contemporaneamente, ad esempio 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 accettano gli stessi argomenti che accettano su un cursore, ma una pipeline li esegue nell’ordine in cui li scrivi. Un $limit prima di $sort mantiene cinque elementi qualsiasi e ordina solo quelli.

challenge icon

Sfida

Facile

Questo passaggio apre il progetto API per lo scaffale di libri predisposto per il corso, anziché i file del tuo progetto di Raggruppamento con $group (incluso server.js). Il lavoro precedente resta nella sua lezione. Leggi i file forniti prima di modificare server.js. Questa copia contiene gli endpoint dello scaffale e il riepilogo del conteggio dei generi.

Lo scaffale vuole un elenco dei libri "più apprezzati": i tre libri con la media di stelle più alta. Un libro deve avere almeno due valutazioni per essere incluso, quindi una fortunata valutazione da 5 stelle non basta a farlo arrivare in cima.

  1. In server.js, aggiungi un endpoint GET /stats/top. Esegue un'aggregazione sulla raccolta ratings, non su books.
  2. Raggruppa le valutazioni per bookId. Ogni gruppo ha tre campi: _id, che è l'id del libro; avg, la media delle sue stars; e count, il numero di valutazioni che ha. Un solo passaggio $group può calcolare sia avg che count.
  3. Mantieni solo i gruppi il cui count è 2 o più. Questa regola riguarda il gruppo di un libro, non una singola valutazione, quindi questo passaggio viene dopo il raggruppamento.
  4. Ordina i gruppi per avg, dal valore più alto al più basso, e mantieni i primi 3.
  5. Rispondi con lo stato 200 e l'array. Ogni elemento ha esattamente questi tre campi:
    [
      { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 },
      { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 }
    ]
    _id è l'_id del libro, quindi un client può recuperare il libro con GET /books/ seguito da quell'id. Se meno di tre libri hanno due valutazioni, l'array è più corto. Prova: ogni esecuzione svuota ratings, quindi invia queste richieste dopo l'ultima esecuzione. Invia GET /books e copia l'_id di Dune, Kindred ed Emma. Invia PUT /books/<Dune's _id>/ratings con { "user": "ada", "stars": 5 }, poi di nuovo con { "user": "bo", "stars": 4 }. Valuta Kindred allo stesso modo, con 3 stelle da ada e 3 da bo. Valuta Emma una sola volta, con { "user": "ada", "stars": 5 }. Invia GET /stats/top: otterrai Dune (avg 4.5, count 2), poi Kindred (avg 3, count 2). Emma non compare, perché ha una sola valutazione. Se Emma compare per prima, il filtro sul conteggio viene eseguito prima del raggruppamento oppure non viene eseguito affatto.

Provalo tu

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

// Ogni errore restituisce { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Restituisce :id come ObjectId, oppure null dopo aver risposto con 400 per un id non valido.
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 iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Database con MongoDB

Esercitati da solo: Playground Express