Menu
CoddyTech

Pipeline Agregasi

Termasuk bagian Database dengan MongoDB dalam perjalanan Node.js di Coddy. Pelajaran 24 dari 29.

Daftar "berperingkat tertinggi" membutuhkan rata-rata, jumlah penilaian minimum, urutan, dan batas. Masing-masing merupakan tahap.

$match mempertahankan dokumen yang lolos filter, seperti find. $avg menghitung rata-rata suatu field di dalam $group. Tahap dijalankan sesuai urutan penulisannya, dan setiap tahap hanya melihat data yang diteruskan oleh tahap sebelumnya. $match sebelum $group memfilter dokumen yang tersimpan. $match setelah $group memfilter kelompok berdasarkan field yang dibuat oleh kelompok tersebut, seperti pada { $match: { orders: { $gte: 3 } } }. Satu $group dapat membuat beberapa field sekaligus, seperti spent: { $sum: '$total' } dan orders: { $sum: 1 }.

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

$sort dan $limit menerima argumen yang sama seperti pada cursor, tetapi pipeline menjalankannya sesuai urutan penulisannya. $limit sebelum $sort mempertahankan lima item apa saja, lalu hanya mengurutkan item tersebut.

challenge icon

Tantangan

Mudah

Langkah ini membuka proyek API rak buku dari kursus yang sudah disiapkan, bukan file proyekmu dari Grouping with $group (termasuk server.js). Pekerjaanmu sebelumnya tetap berada di pelajarannya sendiri. Baca file yang disediakan sebelum mengedit server.js. Salinan ini berisi rute rak buku dan ringkasan jumlah genre.

Rak buku memerlukan daftar "top rated": tiga buku dengan rata-rata bintang tertinggi. Buku perlu memiliki setidaknya dua penilaian agar tercantum, jadi satu penilaian 5 bintang yang beruntung tidak akan menempatkannya di posisi teratas.

  1. Di server.js, tambahkan rute GET /stats/top. Rute ini menjalankan agregasi pada koleksi ratings, bukan pada books.
  2. Kelompokkan penilaian berdasarkan bookId. Setiap kelompok memiliki tiga field: _id, yaitu id buku; avg, rata-rata stars-nya; dan count, jumlah penilaian yang dimilikinya. Satu tahap $group dapat menghitung avg dan count.
  3. Hanya sertakan kelompok yang memiliki count 2 atau lebih. Aturan ini berlaku untuk kelompok sebuah buku, bukan untuk satu penilaian, jadi tahap ini dilakukan setelah pengelompokan.
  4. Urutkan kelompok berdasarkan avg, dari yang tertinggi, lalu ambil 3 pertama.
  5. Berikan respons dengan status 200 dan array tersebut. Setiap entri memiliki tepat tiga field berikut:
    [
      { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 },
      { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 }
    ]
    _id adalah _id buku, jadi client dapat mengambil buku dengan GET /books/ ditambah id tersebut. Jika kurang dari tiga buku memiliki dua penilaian, array-nya lebih pendek. Coba: Setiap kali kamu menekan tombol Jalankan, ratings dikosongkan, jadi kirim permintaan ini setelah terakhir kali kamu menekan tombol Jalankan. Kirim GET /books dan salin _id Dune, Kindred, dan Emma. Kirim PUT /books/<Dune's _id>/ratings dengan { "user": "ada", "stars": 5 }, lalu kirim lagi dengan { "user": "bo", "stars": 4 }. Beri Kindred penilaian dengan cara yang sama, 3 bintang dari ada dan 3 dari bo. Beri Emma satu penilaian dengan { "user": "ada", "stars": 5 }. Kirim GET /stats/top: hasilnya Dune (avg 4.5, count 2), lalu Kindred (avg 3, count 2). Emma tidak tercantum karena dia hanya memiliki satu penilaian. Jika Emma muncul pertama, filter jumlahmu dijalankan sebelum pengelompokan, atau tidak dijalankan sama sekali.

Coba sendiri

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

// Setiap error mengembalikan { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Mengembalikan :id sebagai ObjectId, atau null setelah mengirim respons 400 jika id tidak valid.
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 iconUji dirimu

Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.

Semua pelajaran di Database dengan MongoDB

Latihan sendiri: Playground Express