Menu
CoddyTech

Agregasyon Pipeline'ları

Coddy'nin Node.js Journey'sinin MongoDB ile Veritabanları bölümünün bir parçası. Ders 24 / 29.

“En çok beğenilenler” listesi için ortalamalar, asgari bir puan sayısı, bir sıralama ve bir kesme gerekir. Bunların her biri bir aşamadır.

$match, find gibi bir filtreden geçen belgeleri tutar. $avg, $group içinde bir alanın ortalamasını alır. Aşamalar yazdığınız sırayla çalışır ve her aşama yalnızca kendisinden önceki aşamanın aktardıklarını görür. $group öncesindeki bir $match, depolanan belgeleri filtreler. $group sonrasındaki bir $match, { $match: { orders: { $gte: 3 } } } örneğinde olduğu gibi, grubun oluşturduğu alanlara göre grupları filtreler. Tek bir $group, spent: { $sum: '$total' } ve orders: { $sum: 1 } gibi birden fazla alanı aynı anda oluşturabilir.

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

$sort ve $limit, imleçtekiyle aynı bağımsız değişkenleri alır; ancak bir işlem hattı bunları yazdığınız sırayla çalıştırır. $sort öncesindeki bir $limit, herhangi beş öğeyi tutar ve yalnızca bunları sıralar.

challenge icon

Görev

Kolay

Bu adım, Grouping with $group dersindeki proje dosyaların yerine, kurs için hazırlanmış kitaplık API projesini açar (server.js dahil). Önceki çalışmaların kendi dersinde kalır. server.js dosyasını düzenlemeden önce sağlanan dosyaları oku. Bu kopya, kitaplık rotalarını ve türlere göre sayım özetini içerir.

Kitaplık, “en yüksek puanlı” kitapların bir listesini istiyor: ortalama yıldız puanı en yüksek olan üç kitap. Listede yer alabilmesi için bir kitabın en az iki puan alması gerekir; dolayısıyla şans eseri alınan tek bir 5 yıldızlı puan kitabı en üste taşımaz.

  1. server.js dosyasına bir GET /stats/top rotası ekle. Bu rota, books koleksiyonunda değil, ratings koleksiyonunda bir toplama işlemi çalıştırır.
  2. Puanları bookId değerine göre grupla. Her grupta üç alan bulunur: kitabın kimliği olan _id, stars değerlerinin ortalaması olan avg ve aldığı puanların sayısı olan count. Tek bir $group aşaması hem avg hem de count değerini hesaplayabilir.
  3. Yalnızca count değeri 2 veya daha fazla olan grupları tut. Bu kural tek bir puanla değil, kitabın grubuyla ilgilidir; bu nedenle bu aşama gruplamadan sonra gelir.
  4. Grupları avg değerine göre, en yüksek önce gelecek şekilde sırala ve ilk 3 grubu tut.
  5. 200 durum kodu ve diziyle yanıt ver. Her girdide tam olarak şu üç alan bulunur:
    [
      { "_id": "6720a1f4c2b9e81d3a5f0c11", "avg": 4.5, "count": 2 },
      { "_id": "6720a1f4c2b9e81d3a5f0c12", "avg": 3, "count": 2 }
    ]
    _id, kitabın _id değeridir; böylece istemci GET /books/ adresinin sonuna bu kimliği ekleyerek kitabı getirebilir. İki puan almış kitap sayısı üçten az olduğunda dizi daha kısa olur. Deneyin: Her Run işleminde ratings boşaltılır; bu nedenle bu istekleri son Run işleminden sonra gönderin. GET /books isteğini gönderin ve Dune, Kindred ve Emma'nın _id değerlerini kopyalayın. PUT /books/<Dune's _id>/ratings isteğini { "user": "ada", "stars": 5 } ile gönderin, ardından { "user": "bo", "stars": 4 } ile tekrar gönderin. Kindred'e de aynı şekilde, ada kullanıcısından 3 yıldız ve bo kullanıcısından 3 yıldız verin. Emma'ya bir kez, { "user": "ada", "stars": 5 } ile puan verin. GET /stats/top isteğini gönderin: önce Dune'u (avg 4.5, count 2), ardından Kindred'i (avg 3, count 2) görürsünüz. Emma'nın yalnızca bir puanı olduğundan listede yer almaz. Emma ilk sırada görünüyorsa, sayım filtresi gruplamadan önce çalışıyor ya da hiç çalışmıyor demektir.

Kendin dene

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

// Her hata { error: message } biçiminde yanıt verir.
function sendError(res, status, message) {
    res.status(status).json({ error: message });
}

// Geçerli bir :id için ObjectId; geçersiz bir id için 400 yanıtı verildikten sonra null.
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 iconKendini test et

Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.

MongoDB ile Veritabanları bölümündeki tüm dersler

Kendi başına pratik yap: Express playground