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 đó.
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.
- Trong
server.js, thêm một tuyếnGET /stats/top. Tuyến này chạy phép tổng hợp trên bộ sưu tậpratings, không phải trênbooks. - 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ượtstars; vàcount, số lượt đánh giá sách đó có. Một giai đoạn$groupcó thể tính cảavgvàcount. - Chỉ giữ lại các nhóm có
counttừ 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. - 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. - 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 } ]_idlà_idcủa sách, nên ứng dụng có thể lấy thông tin sách bằng cách ghépGET /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ạchratings, 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ửiGET /booksvà sao chép_idcủa Dune, Kindred và Emma. GửiPUT /books/<Dune's _id>/ratingsvớ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ừadavà 3 sao từbo. Đánh giá Emma một lần với{ "user": "ada", "stars": 5 }. GửiGET /stats/top: bạn sẽ nhận được Dune (avg4.5,count2), rồi đến Kindred (avg3,count2). 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');
});
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
3Tổng hợp dữ liệu
Đếm tài liệuChuỗi các giai đoạnNhóm dữ liệu với $groupTính trung bình trong các nhómLọc kết quả nhómPipeline tổng hợp dữ liệuKết hợp dữ liệu với $lookupTự luyện tập: Playground Express