Upsert, Aggregate avec pyMongo

0 Flares 0 Flares ×

Un petit exemple qui reprend des éléments que j’oublie souvent : Upsert, Aggregate. C’est plus un
mémo pour moi :)

Le but est d’enregistrer des stats pour un article de blog. C’est trés sommaire
car on a un compteur par article par jour. Mais c’est suffisant pour mon mémo.

Le format d’une stat :

> db.stat.find().pretty()
{
    "_id" : ObjectId("51f8c5011a1dd33d650e8edd"),
    "blog_id" : ObjectId("51daaa43261caf11bf83bd00"),
    "count" : 32,
    "date" : ISODate("2013-07-27T00:00:00Z")
}

Tout d’abord, une petite fonction qui facilite le travail avec les dates
avec MongoDB. MongoDB ne gère pas le type python datetime.date.
Donc on va faire illusion avec une datetime avec seulement l’année, le mois,
et les minutes (la minute). En plus, on peut lui passer des arguments (ceux de timedelta)
pour faire une date relative à la date demandée (date du jour par défaut) :

def date_mongo(date=None, **args):
    if not date:
        date = datetime.utcnow()
    d = date.replace(hour=0, minute=0, second=0, microsecond=0)
    if args:
        d = d + timedelta(**args)
    return d

Maintenant, on va ajouter une nouvelle stat. On utilise un upsert ici : si le doc n’existe pas encore
alors mr Mongo en créée un nouveau avec les paramètres de la recherche et ajoute 1 à count qui
n’existe pas donc le met à 1. pour les documents existants il incrémente seulement de 1 count.

def add(blog_id):

    mongo.db["stat"].update(
        {"element": blog_id, "date": date_mongo()},
        {"$inc":{"count":1}},
        True,
        False
    )

Une fonction qui collecte les stats pour un article. Rien de spécial ici, à part le fait de sélectionner
uniquement les champs du doc dont on a besoin avec fields.

Question performance, je me pose la question si il est vraiment judicieux d’utiliser des index ici.
Comme c’est un doc qui peut changer assez souvent (sur un blog différent du mien :) )
le coût engendré par la mise à jour des index dépasse certainement le gain apporté en lecture.
Surtout si les stat sont rarement consultées. TODO: benchmark.

def collect(blog_id, start=date_mongo(days=-28), end=date_mongo()):

    stats = mongo.db["stat"].find(
        {"card": blog_id,
        "date": {"$gte": start, "$lte": end}},
        fields={"_id": False, "card": False}
    ).sort("date", 1)

    return {s["date"].strftime("%Y-%m-%d"): s["count"] for s in stats}

Maintenant, on va faire une petite fonction permettant de récupérer le nombre total de visites
pour un article sur une pèriode donnée. Bon, là, MongoDB est un peu compliqué pour une fonction assez
basique. Il existe peut-être une fonction toute faite mais j’ai pas trouvé ou mal cherché. SInon c’est assez puissant.
J’aurai également pu utiliser un Map/Reduce mais j’ai opté pour la fonction aggregate.
J’ai d’abord filtré les docs (bien mettre avant $group), puis grouper, en faisant la somme des count.

@classmethod
def count(cls, blog_id, start=date_mongo(days=-28), end=date_mongo()):

    stats = mongo.db["stat"].aggregate([
        {
            "$match": {
                "card": blog_id,
                "date": {"$gte": start, "$lte": end}
            }
        },
        {
            "$group": {
                "_id": None,
                "total": {
                    "$sum": "$count"
                }
            }
        }
    ])

    # stats de la forme : { "result" : [ { "_id" : null, "total" : 31 } ], "ok" : 1 }
    ret = 0
    if stats['ok'] > 0:
        if stats["result"]:
            ret = int(stats["result"][0]['total'])

    return ret
0 Flares Twitter 0 Facebook 0 Google+ 0 Buffer 0 0 Flares ×

« »