De l'art de coder un blog (1/2)

(Nhat Minh Lê (rz0) @ 2010-03-01 01:01:34)

Après deux semaines de développement (et une semaine de déploiement qu’il serait dommage d’oublier…), le nouveau blog est là. Pour les pauvres âmes égarées par erreur en ces lieux, permettez-moi de rappeler que ce blog, sur lequel vous êtes, est un produit entièrement artisanal, issu de nos belles campagnes, ahem, je me dissipe.

Mais si je vous écris aujourd’hui, ce n’est pas pour vous parler des nouvelles fonctionnalités trop cools du blog (vous pouvez bien voir par vous-mêmes, vous êtes assez grands), mais pour vous faire part de cette petite expérience, la mienne, dans le monde du développement Web. Au programme donc, aujourd’hui : comment écrire un blog, ou plutôt, comment j’ai écrit le mien. Original, n’est-t-il-pas ?α :]

Avant de poursuivre, il faut replacer la démarche dans son contexte (wow, je croirais entendre mes profs). Mon blog est un petit blog, hébergé en grande partie sur mon PC de bureau reconverti en serveur malgré lui, tout ça derrière une Freebox. Il est clair, à partir de là, que les enjeux ne sont pas du tout comparables à un site moyen ou gros comptant les visiteurs par milliers.

α : Ça me fait penser à ce petit billet gentillet sur lequel j’étais tombé par hasard, un jour, alors que je dérivais de recherches en recherches puis de liens en liens, sur l’Internet : Blogging apps are the new Hello World.

Le fond

Dans le fond, un blog, c’est quoi ? C’est un tas d’articles, de commentaires (si le blog n’est pas trop perdu), et toute une taxinomie de ce vaste petit monde en tags, catégories, séries, ou tout autre mode de classification que l’on pourra imaginer.

Besoins très classiques et à la fois pas si évidents à modéliser correctement.

Bien sûr, le but, du moins le mien, n’est pas d’implémenter tous les outils de filtrage et de tri possibles et imaginables. J’ai toujours pensé, par exemple, que les tags et les catégories étaient redondants. Je ne doute pas que certains y trouvent leur compte, à séparer en tags et en catégories ; moi, honnêtement, avec mon modeste blog, je n’en vois pas l’intérêt. Il n’y a tout simplement pas suffisamment de contenu pour nécessiter autant de sophistication.β

J’ai donc retenu les éléments suivants :

β : Du temps des RZ-pages, mon ancien site, le système de catégorie était autrement plus complexe, conceptuellement, que l’actuel classification par tags : il était hiérarchique, et les articles pouvaient en même temps être des catégories (duh!). L’expérience a montré que pour le contenu que j’avais à proposer, c’était plus confus qu’autre chose.

γ : Du point de vue de l’implémentation, séries et catégories sont proches, si l’on exclut la possibilité de joindre des articles consécutifs d’une série.

Séries, articles, commentaires, et hiérarchie

Durant les dernières vacances (ou était-ce avant ?), j’ai eu une discussion intéressante sur #sdz, sur l’organisation des commentaires. Certains argumentaient que les commentaires hiérarchiques étaient plus clairs, tandis que je restais sceptique.

Mais si je vous parle de cela, c’est qu’un tel choix n’est pas sans conséquences sur l’implémentation. Si l’on opte pour une vue généralisée en arbres des commentaires, il vient naturellement que l’on pourrait faire de même pour les articles, et quitte à faire, ne maintenir qu’un seul arbre gigantesque comprenant toutes les séries, les articles, et les commentaires. Une telle stratégie suggère une représentation spécialisée des données, adaptée à la consultation d’arborescences, telle que la représentation intervallaire.

Pour mon blog, j’ai décidé de rester sur un modèle plus simple, avec une hiérarchie figée, à trois niveaux : séries, articles, commentaires. Se pose tout de même la question de faire une seule ou plusieurs tables ; j’ai décidé de n’en faire qu’une. Un article, une série, ou un commentaire est, dans ma base de données, un enregistrement dans la table entry. Il y a à cela plusieurs raisons, qui peuvent finalement toutes se résumer à un mot : homogénéité. J’aime pouvoir traiter les articles et les commentaires de manière similaire… car ils sont similaires ! L’interface est différente (voy. ci-dessous : Rédaction, mises à jour, et Atom), mais les données sont analogues : des auteurs, un contenu, une date, et quelques autres broutilles.

Mais la conception ne s’arrête pas là : on ne va guère loin avec juste des articles, des commentaires, et des séries… reste à les lier entre eux ! Et c’est là où les choses se gâtent quelque peu. Résumons les différents types de liens qui peuvent lier deux objets :

En SQL, cela donne ceci :

CREATE TABLE entry (
        -- Un identifiant :
        e_eid INTEGER PRIMARY KEY,

        -- Des métadonnées :
        e_atomid TEXT UNIQUE NOT NULL,
        e_title TEXT NOT NULL,
        e_ctime INTEGER NOT NULL,
        e_mtime INTEGER NOT NULL,
        e_lang TEXT,

        -- Quelques liens :
        e_parent INTEGER,
        e_prev INTEGER,
        e_next INTEGER,

        -- Le contenu :
        e_content TEXT,

        -- D'autres métadonnées (relatives au site) :
        e_url TEXT UNIQUE,
        e_type CHAR(1) NOT NULL, -- Series | Article | Topic | Comment.
        e_from CHAR(1) NOT NULL, -- Primary | Secondary | Foreign.
        e_banned CHAR(1) NOT NULL, -- Auto-banned | Banned | -.
        e_level INTEGER NOT NULL,

        -- D'autres champs...
);

De plus, il est fréquent, quand on récupère les données d’un objet, d’avoir besoin de certaines informations connexes, telles que le nombre de commentaires d’un article, ou le titre de la série apparentée. Avec une base de données relationnelle, on a des jointures. C’est un joli concept, dirait bluestorm, mais une jointure pour la série parente, une pour l’article précédent, une autre pour l’article suivant, et encore une pour les commentaires associés, cela fait… beaucoup.

C’est là qu’intervient le cache. Enfin, un cache ; car il y a toute une flopée de façons de faire. On peut mettre en cache le résultat de certaines requêtes, ou carrément la sortie HTML produite par notre script. On peut stocker ça en mémoire (p.ex. en utilisant memcached), ou dans la base de données. Sans compter les mille et une manières de maintenir ce cache à jour.

Dans mon cas, il s’agit tout simplement d’une petite table,δ dont les données sont extraites à partir d’une vue associée. C’est la méthode du pauvre pour matérialiser une vue, disons.

δ : Il y a également un cache au niveau de la sortie HTML (ou XML pour Atom et les sitemaps).

Et pour ceux qui aiment le code, voici :

-- La vue :
CREATE VIEW entry_cache_1 AS
SELECT
    self.e_eid AS ec_eid,
    parent.e_url AS ec_parent_url,
    parent.e_title AS ec_parent_title,
    parent.e_atomid AS ec_parent_atomid,
    parent.e_authors AS ec_parent_authors,
    parent.e_tags AS ec_parent_tags,
    thread.et_utime AS ec_parent_utime,
    prev.e_url AS ec_prev_url,
    prev.e_title AS ec_prev_title,
    prev.e_atomid AS ec_prev_atomid,
    next.e_url AS ec_next_url,
    next.e_title AS ec_next_title,
    next.e_atomid AS ec_next_atomid
FROM
    entry self LEFT JOIN
    entry parent ON self.e_parent = parent.e_eid LEFT JOIN
    entry_thread thread ON self.e_parent = thread.et_eid LEFT JOIN
    entry prev ON self.e_prev = prev.e_eid LEFT JOIN
    entry next ON self.e_next = next.e_eid;

-- La table :
CREATE TABLE entry_cache (
        ec_eid INTEGER PRIMARY KEY,
        ec_parent_url TEXT,
        ec_parent_title TEXT,
        ec_parent_atomid TEXT,
        ec_parent_authors TEXT,
        ec_parent_tags TEXT,
        ec_parent_utime INTEGER,
        ec_prev_url TEXT,
        ec_prev_title TEXT,
        ec_prev_atomid TEXT,
        ec_next_url TEXT,
        ec_next_title TEXT,
        ec_next_atomid TEXT
);

-- Et la requête pour remplir la seconde depuis la première :
REPLACE INTO entry_cache SELECT * FROM entry_cache_1
WHERE ec_eid = ?

En vérité, il manque sur cette démonstration certaines données capitales telles que le nombre de commentaires d’un article. Celles-ci sont en fait maintenues à part, dans une table entry_thread. Cette dernière est mise à jour par incréments, à chaque ajout d’un nouveau commentaire. Elle peut également être rafraîchie à partir d’une requête, à la manière d’entry_cache.ε

Des approches plus complexes (principalement du fait de leurs possibilités de passage à l’échelle) peuvent se justifier pour des gros sites, mais compte tenu de mon hébergement quelque peu technologiquement précaire, pour parler comme Poulet, la ressource limitante est clairement la bande passante, et s’il est rigolo de jouer au petit jeu de l’optimisation un moment, on peut dire que j’aurais déjà perdu assez de temps comme cela. :)

ε : C’est utile, par exemple, pour l’importation de commentaires sous forme d’un flux Atom archivé, comme cela a été le cas lors de la migration vers la nouvelle version du blog.

Et les tags, dans tout ça ?

La question du stockage et de l’indexation des tags est l’autre gros problème d’implémentation. Le sujet est plutôt bien présenté, benchmarks à l’appui, dans cet article sur la représentation des tags dans une base de données ; je ne vais pas épiloguer sur les différentes manières de faire. Pour résumer, on a le choix entre stocker les tags d’un article de manière dénormalisée avec l’objet lui-même, ou utiliser des tables d’associations entre des objets articles et des objets tags, avec toutes les variantes possibles et imaginables.

L’ancienne version du blog utilisait la forme dénormalisée… essentiellement par flemme. Le problème qui s’est posé fut celui de faire des statistiques (p.ex. des nuages de tags) sur les tags. Cette approche est fondamentalement asymétrique et privilégie l’accès par articles au détriment de l’accès par tags.

On pourrait imaginer maintenir un index orthogonal, qui recense tous les articles associés à chaque tag. Avec des structures de données appropriées, l’intuition me dit que l’on devrait arriver à queque chose de pas mal. Je n’ai pas tellement étudié la question, mais il me semble logique que les mecs qui se branlent sur les bases de données non relationnelles, dans lesquelles la redondance est reine, soient enclins à opter pour ce genre de solutions. Quelqu’un au courant pour m’expliquer à quel point j’ai tort ?

Bref, pour revenir à des choses plus modestes, l’architecture actuelle du blog utilise également un modèle redondant : les tags sont stockés à la fois dans la table des articles, sous forme dénormalisée, et dans une table tag, à part, associée à entry par la table d’association tagmap :

CREATE TABLE entry (
        -- Champs précédemment décrits...

        -- Listes d'auteurs et de tags dénormalisées :
        e_authors TEXT,
        e_tags TEXT,

        -- D'autres champs...
);

CREATE TABLE tag (
        t_tid INTEGER PRIMARY KEY,
        t_name VARCHAR(255) UNIQUE NOT NULL
);

CREATE TABLE tagmap (
        tm_eid INTEGER NOT NULL,
        tm_tid INTEGER NOT NULL,
        PRIMARY KEY (tm_eid, tm_tid)
);

Rédaction, mises à jour, et Atom

Le dernier point que j’aimerais aborder est une des particularités de mon blog. En effet, on n’écrit pas ses articles directement sur le site Web ! Mais ce n’est pas un blog entièrement statique non plus…

Mon blog est une espèce hybride qui se nourrit de fichiers Atom. En fait, quand bluestorm ou moi poste un billet, il l’ajoute à un fil Atom interne (grâce à un jeu d’outils en ligne de commandes développés en parallèle), et ping le blog pour lui demander de rafraîchir sa mémoire.

Cela peut paraître un peu compliqué pour pas grand chose, au premier abord, mais l’intérêt de cette méthode est qu’elle découple la rédaction et la présentation du contenu… Pas convaincus ? :p Accessoirement, la généralisation de cette approche permet d’importer du contenu d’autres blogs, le but étant au final d’obtenir une espèce de gros annuaire de billets intéressants affiliés à la micro-communauté #sdz.ζ

ζ : Le concept est différent d’un Planet, qui agrège du contenu trié par date ; ici, le but serait de hiérarchiser, classer les choses intéressantes dans un index global.

Je rappelle au passage aux intéressés, qui se reconnaîtront, que la spéc du format Atom augmenté utilisé pour cette opération de référencement se trouve ici : atom.text

Conclusion

Voilà qui conclut la première partie de cet article. Il me reste pas mal de choses à raconter ; des anecdotes plus légères, plus digestes sans doute pour certains, plus ennuyeuses pour d’autres. J’ai fini de parler de la machinerie derrière le blog. La prochaine fois, je parlerai de la partie directement visible : les questions d’interface, de présentation, de déploiement, tous ces petits ingrédients qui contribuent à créer des petites pages mignonnes et, je l’espère, agréables à lire, pour les visiteurs… sans oublier les robots, moteurs de recherches, et autres joyeusetés qui accompagnent immanquablement tout site Web dans sa vie, ou sa non-vie ! :]