Menu
CoddyTech

Pipeline tổng hợp dữ liệu

Thuộc phần Cơ sở dữ liệu với MongoDB trong hành trình Node.js của Coddy. Bài học 24 trên 29.

Danh sách “được đánh giá cao nhất” cần có điểm trung bình, số lượt đánh giá tối thiểu, thứ tự và giới hạn. Mỗi phần đó là một giai đoạn.

$match giữ lại các tài liệu vượt qua bộ lọc, giống như find. $avg tính trung bình một trường bên trong $group. Các giai đoạn chạy theo thứ tự bạn viết, và mỗi giai đoạn chỉ thấy những gì giai đoạn trước truyền lại. $match đặt trước $group sẽ lọc các tài liệu đã lưu. $match đặt sau $group sẽ lọc các nhóm theo những trường do nhóm tạo ra, như trong { $match: { orders: { $gte: 3 } } }. Một $group có thể tạo nhiều trường cùng lúc, chẳng hạn như spent: { $sum: '$total' } và orders: { $sum: 1 }.

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

$sort và $limit nhận cùng đối số như khi dùng trên một con trỏ, nhưng pipeline chạy chúng theo thứ tự bạn viết. Đặt $limit trước $sort sẽ giữ lại năm mục bất kỳ và chỉ sắp xếp những mục đó.

challenge icon

Thử thách

Dễ

Bước này mở dự án API giá sách đã chuẩn bị sẵn cho khóa học, thay vì các tệp dự án của bạn từ Nhóm với $group (bao gồm cả server.js). Phần bạn đã làm trước đó vẫn nằm trong bài học riêng. Hãy đọc các tệp được cung cấp trước khi chỉnh sửa server.js. Bản sao này có các tuyến của giá sách và bản tóm tắt số lượng theo thể loại.

Giá sách cần một danh sách "được đánh giá cao nhất": ba cuốn sách có điểm sao trung bình cao nhất. Sách cần có ít nhất hai lượt đánh giá mới được liệt kê, vì vậy một lượt đánh giá 5 sao may mắn không thể đưa sách lên đầu danh sách.

  1. Trong server.js, thêm một tuyến GET /stats/top. Tuyến này chạy phép tổng hợp trên bộ sưu tập ratings, không phải trên books.
  2. Nhóm các lượt đánh giá theo bookId. Mỗi nhóm có ba trường: _id, là mã định danh của sách; avg, điểm trung bình của các lượt stars; và count, số lượt đánh giá sách đó có. Một giai đoạn $group có thể tính cả avg và count.
  3. Chỉ giữ lại các nhóm có count từ 2 trở lên. Quy tắc này áp dụng cho nhóm của một cuốn sách, không phải cho một lượt đánh giá, vì vậy giai đoạn này phải đặt sau bước nhóm.
  4. Sắp xếp các nhóm theo avg, từ cao xuống thấp, rồi giữ lại 3 nhóm đầu tiên.
  5. Trả về mã trạng thái 200 cùng mảng kết quả. Mỗi phần tử có đúng ba trường sau:
    [
      { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 },
      { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 }
    ]
    _id là _id của sách, nên ứng dụng có thể lấy thông tin sách bằng cách ghép GET /books/ với mã định danh đó. Khi có ít hơn ba cuốn sách được đánh giá ít nhất hai lần, mảng sẽ ngắn hơn. Hãy thử: Mỗi lần nhấn Chạy sẽ xóa sạch ratings, vì vậy hãy gửi các yêu cầu này sau lần nhấn Chạy cuối cùng. Gửi GET /books và sao chép _id của Dune, Kindred và Emma. Gửi PUT /books/<Dune's _id>/ratings với { "user": "ada", "stars": 5 }, rồi gửi lại với { "user": "bo", "stars": 4 }. Đánh giá Kindred theo cách tương tự, với 3 sao từ ada và 3 sao từ bo. Đánh giá Emma một lần với { "user": "ada", "stars": 5 }. Gửi GET /stats/top: bạn sẽ nhận được Dune (avg 4.5, count 2), rồi đến Kindred (avg 3, count 2). Emma không xuất hiện vì sách này chỉ có một lượt đánh giá. Nếu Emma đứng đầu, nghĩa là bộ lọc số lượt đánh giá chạy trước bước nhóm hoặc hoàn toàn không chạy.

Tự làm thử

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

// Mỗi lỗi trả về { error: message }.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Chuyển :id thành ObjectId, hoặc trả về null sau khi phản hồi 400 nếu id không hợp lệ.
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 iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Cơ sở dữ liệu với MongoDB

Tự luyện tập: Playground Express