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.
Sfida
FacileQuesto 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.
- In
server.js, aggiungi un endpointGET /stats/top. Esegue un'aggregazione sulla raccoltaratings, non subooks. - Raggruppa le valutazioni per
bookId. Ogni gruppo ha tre campi:_id, che è l'id del libro;avg, la media delle suestars; ecount, il numero di valutazioni che ha. Un solo passaggio$grouppuò calcolare siaavgchecount. - 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. - Ordina i gruppi per
avg, dal valore più alto al più basso, e mantieni i primi 3. - 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'_iddel libro, quindi un client può recuperare il libro conGET /books/seguito da quell'id. Se meno di tre libri hanno due valutazioni, l'array è più corto. Prova: ogni esecuzione svuotaratings, quindi invia queste richieste dopo l'ultima esecuzione. InviaGET /bookse copia l'_iddi Dune, Kindred ed Emma. InviaPUT /books/<Dune's _id>/ratingscon{ "user": "ada", "stars": 5 }, poi di nuovo con{ "user": "bo", "stars": 4 }. Valuta Kindred allo stesso modo, con 3 stelle daadae 3 dabo. Valuta Emma una sola volta, con{ "user": "ada", "stars": 5 }. InviaGET /stats/top: otterrai Dune (avg4.5,count2), poi Kindred (avg3,count2). 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');
});
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Database con MongoDB
1Documenti
Un libro come documentoRaccolte e databaseConnessione a MongoDBLettura dei documenti memorizzatiInserimento di documentiRicerca di un documentoRicerca tramite ObjectIdConfronto tra numeriFiltri di queryScelta dei campi restituitiOrdinamento dei risultati della queryMantenere i primi risultatiProiezione, ordinamento e limite3Aggregazione
Conteggio dei documentiUna pipeline di fasiRaggruppare con $groupMedie all'interno dei gruppiFiltrare i risultati dei gruppiPipeline di aggregazioneUnire con $lookupEsercitati da solo: Playground Express