Filtre actif, cliquez pour en enlever un tag :
Cliquez sur un tag pour affiner votre recherche :
Résultat de la recherche (7 notes) :
Journal du mardi 26 août 2025 à 21:45
Voici une note pour présenter la seconde #iteration du Projet 32 - "POC serveur Git HTTP avec exécution de scripts au push".
J'ai tout d'abord implémenté dans ce commit un mécanisme qui exécute du code JavaScript automatiquement après chaque git push.
Pour cela, j'ai choisi de me baser sur le mécanisme Server-Side Hooks natif de Git : post-receive.
Voici le contenu de ce script hook en Bash :
#!/bin/bash
while read oldrev newrev refname; do
branch=$(git rev-parse --symbolic --abbrev-ref $refname)
curl -X POST \
-H "Content-Type: application/json" \
-d "{
\"oldrev\": \"${oldrev}\",
\"newrev\": \"${newrev}\",
\"refname\": \"${refname}\",
\"branch\": \"${branch}\",
\"repository\": \"$(basename $(pwd))\"
}" \
"${POST_RECIEVE_HOOK_URL}" >> /dev/null
done
Chaque événement git push déclenche un appel HTTP vers le endpoint http://localhost:3334/post_recieve_hook_url/ exposé par le serveur NodeJS. Le payload contient notamment :
oldrev: l'identifiant du dernier commit présent dans le repository avant le pushnewrev: l'identifiant du commit le plus récent envoyé lors du push
L'intervalle entre oldrev et newrev permet d'identifier précisément l'ensemble des commits qui ont été poussés lors de cette opération.
J'ai ensuite implémenté une version SvelteKit iso-fonctionnelle de node-git-http-server. Voici le repository poc-node-git-server-in-sveltekit .
Contrairement à ce que j'avais prévu initialement, pour cette implémentation, je ne me suis pas basé sur SvelteKit Custom Server, mais sur la fonctionnalité Server hooks : src/hooks.server.js#L11.
Journal du dimanche 24 août 2025 à 12:42
Je viens de publier la première #iteration du Projet 32 - "POC serveur Git HTTP avec exécution de scripts au push" dans le repository node-git-http-server.
L'implémentation d'un serveur Git HTTP via Apache ou nginx, en s'appuyant sur git-http-backend , paraît plutôt simple à réaliser.
Comme mon objectif est d'intégrer cette fonctionnalité dans le projet sklein-pkm-engine et que j'ai une préférence pour les monolith, j'ai exploré les solutions basées sur NodeJS.
J'ai dans un premier temps étudié le projet node-git-http-server et ensuite node-git-server.
Ces deux projets semblent peu actifs.
J'ai échoué à faire fonctionner le projet node-git-server, probablement à cause d'une erreur de ma part — j'ai sans doute oublié d'initialiser au préalable les dépôts Git en mode bare.
Par la suite, en utilisant Claude Sonnet 4, j'ai créé une implémentation basée uniquement sur les modules natifs de NodeJS et l'exécutable git-http-backend , sans recourir à aucun package NodeJS externe.
Voici le résultat : node-git-http-server/server.js.
Prochaines étapes
- Implémenter un système qui exécute du code JavaScript automatiquement après chaque
git push, en lui transmettant la branche concernée et la liste des nouveaux commits publiés. - Implémenter une déclinaison de ce projet dans un SvelteKit Custom Server.
Réflexions à propos de Node.js et de JavaScript plus globalement
Le 18 avril 2012, je publiais sur LinuxFr le journal qui suit — il recueillit 91 commentaire (lien https://linuxfr.org/users/harobed/journaux/reflexions-a-propos-de-nodejs-et-de-javascript-plus-globalement)
Bonjour,
Cela fait quelques jours que je me pose la question suivante : « Est-ce que Node.js ne va pas devenir une technologie incontournable / majeure dans les 2 ans qui viennent ? »
Le contexte
Je suis développeur Python depuis de nombreuses années. J'aime ses librairies, j'aime ses outils, j'aime sa communauté.
J'aime tellement sa syntaxe que quand je vois la syntaxe d'autres langages, j'ai une réaction quelque peu épidermique à la lecture du code.
Avec le temps, l'habitude de la syntaxe Python minimaliste, proche d'un pseudo-code, rend difficile la possibilité d'apprécier un autre langage. Je ne sais pas si c'est positif ou négatif, je me pose simplement la question. Enfin, ceci est un autre sujet.
Cependant, mon regard se tourne de plus en plus vers Node.js… je n'ai pas franchi le pas, je n'ai rien développé en Node.js… mais je me demande si je ne passe pas à côté de quelque chose d'important.
Les forces de Node.js
-
Bien que Node.js date seulement de 2009 :
- il a déjà un bon système de paquets : Node Package Manager, que la grande majorité de la communauté semble utiliser
- il a déjà une très forte communauté, de très nombreuses librairies — exemple : plein de librairies classées, déjà plus de 9 000 paquets sur npm
- de plus en plus de sociétés semblent l'utiliser
-
Tous les développeurs web (front-end) connaissent le langage, il est très populaire. Par conséquent, pour passer à Node.js il n'y a pas le frein d'apprendre un nouveau langage, comme ce serait le cas avec l'utilisation d'un framework web basé sur Ruby ou Python. Je pense que dans les mois qui viennent, de nombreux développeurs PHP vont passer à Node.js… surtout ceux qui regardaient ailleurs mais qui ne souhaitent pas apprendre un nouveau langage.
-
La possibilité de partager du code (librairies communes…) entre la partie client et serveur peut être intéressante. La question se pose de plus en plus, étant donné qu'on est de plus en plus amené à effectuer beaucoup de traitements côté client (exemple avec Backbone.js).
Dernièrement, dans tous mes projets de développement web, j'ai utilisé un moteur de template côté client en plus du moteur de template côté serveur.
Je dois gérer de plus en plus souvent une couche modèle côté client, des vues côté client… Cela fait donc plein d'éléments en double, utilisés une fois sur le serveur, une fois sur le client. Donc deux technologies à maîtriser.
Ces derniers jours, la visualisation du screencast du framework JavaScript Meteor m'a mis la puce à l'oreille.
Autre exemple : il est possible d'utiliser la même API Canvas côté client et côté serveur (avec node-canvas). -
Node.js semble être très rapide et tient très bien la montée en charge.
-
La vitesse de l'interpréteur JavaScript est en constante progression : V8 (JavaScript engine)
Faiblesses de Node.js
Je ne sais pas si mes remarques sont exactes, je n'ai aucune expérience en Node.js.
- Par exemple, dans la partie Bad Use Cases du guide Felix's Node.js Convincing the boss guide, il est indiqué que les frameworks Node.js sont moins matures que ce que l'on peut trouver en Ruby, Python et PHP (bon, pour ce dernier j'ai des doutes — je pense que la communauté Node.js est déjà plus mature, plus structurée que la communauté PHP… enfin bon…).
- Je ne pense pas que Sequelize soit aussi mature que SQLAlchemy.
- Je n'ai pas trouvé d'outil comme Whoosh en JavaScript (c'est un moteur de recherche léger). J'ai tout de même trouvé Node-xapian.
Je ne sais pas s'il est encore tôt pour passer à Node.js, mais si la communauté continue à être aussi dynamique que ces 2 dernières années… alors les pièces manquantes seront bientôt créées.
Est-ce que l'on va retrouver JavaScript partout ?
Voici une petite liste :
- JavaScript dans les navigateurs web
- JavaScript côté serveur (Node.js)
- JavaScript dans Qt : Introduction QML + Qt Quick, qui semble vraiment très puissant
- Les possibilités d'utiliser GTK + JS : gjs and Gtk+
- GNOME Shell est en grande partie développé en JavaScript. Ses extensions sont entièrement développées en JS.
- Sur mobile : WebOS et Boot to Gecko
Quand je vois cette liste, je me pose sérieusement la question : est-ce que JavaScript est le langage incontournable de ces 10 prochaines années ? Certes, il est déjà incontournable dans le navigateur, mais pour le reste ?
Dépôt GitHub : https://github.com/pyodide/pyodide
Dépôt GitHub : https://github.com/kelektiv/node-cron/
Site officiel : https://www.passportjs.org/
Date de la création de cette note : 9 septembre 2026.
Quel est l'objectif de ce projet ?
Je souhaite créer un POC pour tester et apprendre à construire un pipeline Retrieval-augmented generation (RAG) complet en NodeJS, avec des briques de base comme pgvector, pg_search (BM25), le reranking et AI SDK.
Je souhaite tester mon RAG avec les documents suivants :
- Système national d'économie politique (1841) de Friedrich List
- Sophismes économiques (1845-1848) de Frédéric Bastiat
Ces deux livres sont disponibles en Creative Commons sur Wikisource.
Ce projet sera découpé en 4 composants :
- la préparation des documents sources et la génération des résumés hiérarchiques
- le découpage en paragraphes, la génération des sidecars JSONL et des embeddings, et leur versionnement dans git
- le chargement en base et l'indexation (modèle de données PostgreSQL)
- le service de recherche MCP du RAG
Composant 1 : Préparation des documents et génération des résumés hiérarchiques
Je connaissais RAPTOR, mais en creusant le sujet avec Sonnet 5, j'ai compris que RAPTOR n'est pas une technique concurrente de la hierarchical summarization : c'est un type de hierarchical summarization, qui construit son arbre par clustering.
RAPTOR prépare les embeddings avant de les regrouper : avec Uniform Manifold Approximation and Projection (UMAP), il rapproche les contenus similaires dans un espace simplifié ; avec Gaussian Mixture Model (GMM), il identifie des groupes de contenus proches ; il fait résumer chaque groupe par un LLM, puis recommence récursivement. Cette approche est bien adaptée à un corpus dont on ignore la structure.
Or mes sources sont des livres : des documents dont la structure (livres, chapitres) est connue d'avance et reflète l'organisation des idées. Plutôt que de redécouvrir cette structure par clustering, je pense qu'il est plus simple, plus fiable, plus rapide et moins gourmand en ressources de s'appuyer directement dessus. J'ai découvert avec DeepSeek que cette technique se nomme hierarchical summarization structurel (structure-driven).
Une indexation de type hierarchical summarization structurel consistera à construire un arbre de nœuds : le niveau 0 est le contenu source découpé en paragraphes — il n'est pas résumé — et un LLM génère les résumés des niveaux 1 à 3 :
- Niveau 0 : le contenu source, tel qu'extrait des fichiers sources, découpé en paragraphes — ce sont les feuilles de l'arbre, pas des résumés (aucun LLM ici)
- Niveau 1 : un résumé par section de premier niveau — chapitre, ou pièce liminaire rattachée directement à l'ouvrage (préface, introduction) —, généré par défaut à partir des nœuds de niveau 0 de cette section
- Niveau 2 : un résumé par partie de l'ouvrage — nommée « livre » chez List, « série » chez Bastiat —, généré par défaut à partir des résumés de niveau 1
- Niveau 3 : un résumé de l'ouvrage entier, généré par défaut à partir des résumés de niveau 2 des parties et des résumés de niveau 1 des pièces liminaires
Le niveau d'un nœud décrit sa position dans l'arbre de résumés (son degré d'abstraction), pas l'origine de sa génération.
Par défaut, chaque résumé est donc construit à partir des résumés du niveau précédent. Je souhaite pouvoir configurer un algorithme alternatif, plus gourmand en ressources LLM, mais rendu techniquement possible par les modèles LLM qui supportent de très grandes context windows, où certains résumés sont construits à partir d'une source plus directe :
- le niveau 2 est construit à partir du contenu de tous les chapitres de la partie, sans passer par les résumés du niveau 1
- le niveau 3 est construit à partir des résumés de niveau 1, sans passer par les résumés du niveau 2
Je pense que l'un de mes défis sera de concevoir un bon prompt pour générer les résumés : le résumé doit être fidèle au contenu tout en servant mon angle d'analyse, et le prompt devra sans doute s'adapter à la thématique du livre et au niveau du résumé (section, partie, œuvre).
corpus/
├── list-systeme-national-economie-politique/
│ ├── summary.md # résumé de l'œuvre (niveau 3)
│ ├── preface/ # préface de l'auteur (le paratexte du traducteur est exclu du corpus)
│ │ ├── source.md # texte de la préface
│ │ └── summary.md # résumé de la préface (niveau 1)
│ ├── introduction/
│ │ ├── source.md
│ │ └── summary.md # résumé de l'introduction (niveau 1)
│ ├── livre-1-l-histoire/
│ │ ├── summary.md # résumé du "Livre premier" (niveau 2)
│ │ ├── chapitre-01-les-italiens/
│ │ │ ├── source.md
│ │ │ └── summary.md # résumé du chapitre (niveau 1)
│ │ ├── chapitre-02-les-anseates/
│ │ │ ├── source.md
│ │ │ └── summary.md
│ │ └── ...
│ ├── livre-2-la-theorie/
│ │ ├── summary.md
│ │ ├── chapitre-01-l-economie-politique-et-l-economie-cosmopolite/
│ │ │ ├── source.md
│ │ │ └── summary.md
│ │ └── ...
│ ├── livre-3-les-systemes/
│ │ ├── summary.md
│ │ ├── chapitre-01-les-economistes-italiens/
│ │ │ ├── source.md
│ │ │ └── summary.md
│ │ └── ...
│ └── livre-4-la-politique/
│ ├── summary.md
│ ├── chapitre-01-la-suprematie-insulaire-et-les-puissances-continentales/
│ │ ├── source.md
│ │ └── summary.md
│ └── ...
└── bastiat-sophismes-economiques/
├── summary.md # résumé de l'œuvre (niveau 3)
├── premiere-serie/
│ ├── summary.md # résumé de la première série (niveau 2)
│ ├── chapitre-01-abondance-disette/
│ │ ├── source.md
│ │ └── summary.md # résumé du chapitre (niveau 1)
│ └── ...
└── deuxieme-serie/
├── summary.md # résumé de la deuxième série (niveau 2)
├── chapitre-01-physiologie-de-la-spoliation/
│ ├── source.md
│ └── summary.md
└── ...
Je pense ajouter dans le frontmatter des documents sources Markdown un indicateur pour marquer explicitement les documents à vectoriser ou non.
Chaque fichier source.md ou summary.md du corpus porte aussi dans son frontmatter le champ parent_source_file : le chemin, relatif au corpus, du fichier du nœud parent dans l'arbre de résumés. Ce champ suit la règle de génération des résumés : un source.md a pour parent le summary.md de sa section (celui du même dossier) ; un summary.md de chapitre a pour parent le summary.md de la partie qui le contient ; un summary.md de pièce liminaire (préface, introduction) ou de partie a pour parent le summary.md de l'œuvre ; le summary.md de l'œuvre, racine de l'arbre, n'a pas de parent.
Composant 2 : Génération des sidecars et versionnement des embeddings
La fonction de ce composant est de découper en paragraphes le contenu des fichiers Markdown du composant 1, de vectoriser les fichiers que le frontmatter marque à vectoriser, et d'enregistrer le résultat dans des fichiers sidecar à côté des fichiers sources, pour les versionner dans git. Un sidecar est généré pour chaque fichier, qu'il soit vectorisé ou non ; le sidecar d'un fichier non vectorisé contient les paragraphes sans embeddings (voir l'exemple plus bas). Le composant 2 recopie aussi dans le manifest de chaque sidecar le champ parent_source_file lu dans le frontmatter du fichier .md (renseigné par le composant 1).
Voici l'équivalent de la précédente arborescence de fichiers avec en plus les fichiers sidecars :
# Sidecars *.md.jsonl : un sidecar est généré pour chaque fichier, vectorisé ou non.
# Chaque paragraphe du fichier donne une ligne ; les fichiers vectorisés contiennent des
# champs embeddings, les autres non (cas « tout vectorisé » illustré ici — voir exemple JSONL plus bas).
corpus/
├── list-systeme-national-economie-politique/
│ ├── summary.md # résumé de l'œuvre (niveau 3)
│ ├── summary.md.jsonl
│ ├── preface/ # préface de l'auteur (le paratexte du traducteur est exclu du corpus)
│ │ ├── source.md
│ │ ├── source.md.jsonl
│ │ ├── summary.md
│ │ └── summary.md.jsonl
│ ├── introduction/
│ │ ├── source.md
│ │ ├── source.md.jsonl
│ │ ├── summary.md
│ │ └── summary.md.jsonl
│ ├── livre-1-l-histoire/
│ │ ├── summary.md # résumé du "Livre premier" (niveau 2)
│ │ ├── summary.md.jsonl
│ │ ├── chapitre-01-les-italiens/
│ │ │ ├── source.md
│ │ │ ├── source.md.jsonl
│ │ │ ├── summary.md # résumé du chapitre (niveau 1)
│ │ │ └── summary.md.jsonl
│ │ ├── chapitre-02-les-anseates/
│ │ │ ├── source.md
│ │ │ ├── source.md.jsonl
│ │ │ ├── summary.md
│ │ │ └── summary.md.jsonl
│ │ └── ...
│ ├── livre-2-la-theorie/
│ │ ├── summary.md
│ │ ├── summary.md.jsonl
│ │ ├── chapitre-01-l-economie-politique-et-l-economie-cosmopolite/
│ │ │ ├── source.md
│ │ │ ├── source.md.jsonl
│ │ │ ├── summary.md
│ │ │ └── summary.md.jsonl
│ │ └── ...
│ ├── livre-3-les-systemes/
│ │ ├── summary.md
│ │ ├── summary.md.jsonl
│ │ ├── chapitre-01-les-economistes-italiens/
│ │ │ ├── source.md
│ │ │ ├── source.md.jsonl
│ │ │ ├── summary.md
│ │ │ └── summary.md.jsonl
│ │ └── ...
│ └── livre-4-la-politique/
│ ├── summary.md
│ ├── summary.md.jsonl
│ ├── chapitre-01-la-suprematie-insulaire-et-les-puissances-continentales/
│ │ ├── source.md
│ │ ├── source.md.jsonl
│ │ ├── summary.md
│ │ └── summary.md.jsonl
│ └── ...
└── bastiat-sophismes-economiques/
├── summary.md # résumé de l'œuvre (niveau 3)
├── summary.md.jsonl
├── premiere-serie/
│ ├── summary.md # résumé de la première série (niveau 2)
│ ├── summary.md.jsonl
│ ├── chapitre-01-abondance-disette/
│ │ ├── source.md
│ │ ├── source.md.jsonl
│ │ ├── summary.md # résumé du chapitre (niveau 1)
│ │ └── summary.md.jsonl
│ └── ...
└── deuxieme-serie/
├── summary.md # résumé de la deuxième série (niveau 2)
├── summary.md.jsonl
├── chapitre-01-physiologie-de-la-spoliation/
│ ├── source.md
│ ├── source.md.jsonl
│ ├── summary.md
│ └── summary.md.jsonl
└── ...
Voici ci-dessous un exemple de fichier sidecar JSONL pour un fichier Markdown source vectorisé. Le découpage s'effectue par paragraphes : chaque paragraphe devient un chunk, sauf si un paragraphe dépasse le seuil de tokens défini (voir composant 2), auquel cas il est subdivisé. Le champ source_file_hash est abrégé ici pour la lisibilité ; il porte en réalité le SHA-256 complet du fichier. Chaque manifest porte aussi parent_source_file, le chemin relatif au corpus du fichier du nœud parent ; ce champ est absent (NULL) pour la racine de l'arbre — le summary.md de l'œuvre.
{
"type": "manifest",
"source_file": "corpus/list-systeme-national-economie-politique/livre-1-l-histoire/chapitre-01-les-italiens/source.md",
"source_file_hash": "sha256:2cf24d…",
"parent_source_file": "corpus/list-systeme-national-economie-politique/livre-1-l-histoire/chapitre-01-les-italiens/summary.md",
"heading_path": "Système national d'économie politique > Livre I > Chapitre I (Les Italiens)",
"level": 0,
"node_type": "source",
"embedding_model": "text-embedding-3-small",
"dimension": 1536,
"chunk_count": 3
}
{
"chunk_index": 0,
"content_token_count": 782,
"text": "…texte exact du premier chunk embeddé…",
"embedding": [-0.013, 0.022, 0.107, -0.041, …]
}
{
"chunk_index": 1,
"content_token_count": 756,
"text": "…texte exact du deuxième chunk embeddé…",
"embedding": [0.041, -0.017, 0.094, 0.002, …]
}
{
"chunk_index": 2,
"content_token_count": 803,
"text": "…texte exact du troisième chunk embeddé…",
"embedding": [-0.007, 0.011, -0.052, 0.018, …]
}
Voici l'exemple d'un fichier non vectorisé : le sidecar est bien généré, avec le même découpage par paragraphes, mais le manifest ne porte ni embedding_model ni dimension et aucune ligne ne contient d'embedding. Ces paragraphes ne sont retrouvables qu'en BM25.
{
"type": "manifest",
"source_file": "corpus/bastiat-sophismes-economiques/premiere-serie/chapitre-01-abondance-disette/source.md",
"source_file_hash": "sha256:f53b7d…",
"parent_source_file": "corpus/bastiat-sophismes-economiques/premiere-serie/chapitre-01-abondance-disette/summary.md",
"heading_path": "Sophismes économiques > Première série > Chapitre I (Abondance, disette)",
"level": 0,
"node_type": "source",
"chunk_count": 2
}
{
"chunk_index": 0,
"content_token_count": 612,
"text": "…texte exact du premier paragraphe…"
}
{
"chunk_index": 1,
"content_token_count": 538,
"text": "…texte exact du deuxième paragraphe…"
}
Composant 3 : Chargement en base et indexation (pgvector + pg_search)
Je souhaite importer tout cela dans une base de données PostgreSQL configurée avec les extensions suivantes : pgvector pour la recherche sémantique vectorielle et pg_search pour la recherche BM25.
Exemple de schéma de modèle de données :
CREATE TABLE hierarchical_content_nodes (
id SERIAL PRIMARY KEY,
-- Profondeur dans la hiérarchie, du contenu source vers les résumés les plus abstraits.
-- Convention générale (adaptable selon la profondeur réelle de chaque ouvrage) :
-- 0 = contenu source (un paragraphe extrait du fichier source)
-- 1 = résumé d'une section de premier niveau (chapitre, ou pièce liminaire comme
-- la préface ou l'introduction), généré par défaut à partir des nœuds de niveau 0
-- de cette section
-- 2 = résumé d'une partie de l'ouvrage (livre chez List, série chez Bastiat), généré
-- par défaut à partir des résumés de niveau 1
-- 3 = résumé de l'ouvrage entier, généré par défaut à partir des résumés de niveau 2
-- des parties et des résumés de niveau 1 des pièces liminaires
level INT NOT NULL,
-- Chemin relatif au corpus du fichier du nœud parent dans l'arbre de résumés
-- (champ `parent_source_file` du manifest, recopié du frontmatter du .md).
-- Un nœud = un fichier : toutes les lignes d'un même `source_file` (les
-- paragraphes d'un résumé éventuellement multi-paragraphes) forment un seul
-- nœud, et les lignes d'un même nœud partagent donc le même parent. NULL pour
-- la racine (summary.md de l'œuvre, niveau 3). Pas de contrainte de clé
-- étrangère possible : `source_file` n'est pas unique dans cette table ;
-- l'intégrité (existence du sidecar du fichier parent) est vérifiée à l'import.
parent_source_file TEXT,
node_type TEXT NOT NULL CHECK (node_type IN ('source', 'summary')),
-- Fichier Markdown (source.md ou summary.md) dont est issu ce nœud (sortie du
-- composant 1), chemin relatif au corpus. Identique au champ `source_file` du manifest
-- du sidecar JSONL du composant 2.
source_file TEXT NOT NULL,
-- Hash SHA-256 du fichier Markdown au moment de la génération du sidecar
-- (champ `source_file_hash` du manifest). Détecte un .md modifié
-- depuis sa dernière indexation → re-génération des lignes correspondantes.
source_file_hash TEXT NOT NULL,
-- Index du paragraphe dans son fichier source (champ `chunk_index` du JSONL),
-- de 0 à chunk_count-1. Un paragraphe est un chunk ; un paragraphe trop long
-- est subdivisé en plusieurs chunks successifs.
chunk_index INT NOT NULL,
-- Chemin hiérarchique lisible du nœud (champ `heading_path` du manifest),
-- constant pour toutes les lignes issues d'un même fichier.
-- Exemples (conformes à la convention de niveau ci-dessus) :
-- 'Sophismes économiques > Première série > Chapitre I' (level 0, source)
-- 'Sophismes économiques > Première série > Chapitre I (résumé)' (level 1, summary)
-- 'Sophismes économiques > Première série (résumé)' (level 2, summary)
-- 'Sophismes économiques (résumé)' (level 3, summary)
-- 'Système national d'économie politique > Préface' (level 0, source)
-- 'Système national d'économie politique > Préface (résumé)' (level 1, summary)
heading_path TEXT,
-- Contenu du nœud = champ `text` de la ligne JSONL correspondante :
-- - nœud `summary` → un paragraphe du résumé généré par le LLM ;
-- - nœud `source` → un paragraphe du fichier source (ou un fragment si le
-- paragraphe a dû être subdivisé).
content TEXT NOT NULL,
-- Nombre de tokens de `content` (champ `content_token_count` du JSONL),
-- calculé une fois à l'ingestion pour éviter de re-tokenizer à chaque requête.
-- Sert de garde-fou au moment du retrieval : décider si un nœud peut être
-- chargé tel quel dans le prompt, ou s'il faut charger ses enfants à la place
-- (limite de contexte du LLM, limite de tokens par document du reranker).
-- Un nœud pouvant couvrir plusieurs lignes (résumé multi-paragraphes), son
-- total se calcule en sommant `content_token_count` sur les lignes de son
-- `source_file`. « Charger les enfants » d'un nœud = lire les lignes dont
-- `parent_source_file` pointe vers ce fichier.
content_token_count INT,
-- Nom du modèle d'embedding utilisé (champ `embedding_model` du manifest).
-- Modèle unique pour tout le corpus à un instant donné (cf. dimension 1536) ;
-- la politique par fichier porte sur le choix de vectoriser ou non, pas sur le modèle.
-- Un changement de modèle global invalide tous les vecteurs : le source_file_hash ne
-- changeant pas dans ce cas, l'import doit comparer le modèle lu dans le sidecar à
-- celui stocké pour déclencher la re-génération par le composant 2.
embedding_model TEXT,
-- Embedding du paragraphe, NULL si le fichier n'est pas vectorisé
-- (politique d'embedding par fichier, exprimée dans le frontmatter du .md).
-- Un nœud sans embedding n'est retrouvable qu'en BM25 (pg_search) sur `content`.
embedding vector(1536),
created_at TIMESTAMPTZ DEFAULT now(),
-- Import = lecture d'un sidecar JSONL (sortie du composant 2), un sidecar par fichier,
-- vectorisé ou non. Le manifest (1re ligne) porte les champs communs au fichier
-- (source_file, source_file_hash, parent_source_file, heading_path, level, node_type ; embedding_model et
-- dimension si le fichier est vectorisé) ; chaque ligne suivante (un paragraphe) devient
-- une ligne de cette table, héritant des champs de son manifest.
-- L'import est rejouable : à la re-génération d'un fichier, ses lignes sont d'abord
-- supprimées (DELETE WHERE source_file = …) puis réinsérées, car la contrainte UNIQUE
-- (source_file, chunk_index) ne suffit pas si le découpage change le nombre de chunks.
UNIQUE (source_file, chunk_index)
);
Par défaut, je vectorise l'ensemble des documents — contenus sources et résumés — pour permettre la recherche sémantique vectorielle, tout en les indexant aussi en BM25 (pg_search) pour la recherche lexicale. Un fichier .md.jsonl est généré pour chaque fichier ; le sidecar d'un fichier non vectorisé contient les paragraphes sans embeddings, retrouvables uniquement en BM25.
Pour réduire le coût de génération des embeddings et accélérer le traitement, une variante économique consiste à ne pas vectoriser certains niveaux et à ne laisser sur ceux-ci que la recherche BM25. Par exemple : pas d'embedding sur le contenu brut des chapitres, recherche BM25 au niveau des paragraphes, recherche vectorielle réservée aux résumés (sections, parties, œuvre), quitte à charger ensuite les enfants d'un nœud retenu plutôt que le nœud entier.
Composant 4 : Service de recherche MCP du RAG
Après cela, je compte m'inspirer de mes projets sveltekit-ssr-ai-sdk-poc et toggl-pg-mirror pour implémenter le service MCP de recherche d'information dans le RAG.
Contrairement à ces deux exemples de service MCP, cette fois, je pense que je ne pourrai pas laisser l'agent IA générer en autonomie le code SQL pour faire ses recherches étant donné que la recherche devra vectoriser des chaînes de recherche et effectuer optionnellement du reranking en sortie.
La recherche s'effectue sur les paragraphes (lignes de la table, BM25 et/ou vectorielle selon la politique). Quand un paragraphe est retenu, je prévois de remonter à son nœud parent pour fournir au LLM un contexte élargi plutôt qu'un paragraphe isolé : le nœud parent est le fichier parent_source_file de la ligne retenue — résumé de section, de partie ou de l'œuvre selon la position — et la remontée consiste à charger toutes les lignes dont le source_file correspond à ce chemin. Un paragraphe du nœud racine (résumé de l'œuvre, niveau 3) n'a pas de parent.
Le choix du modèle de reranking (et sa limite de tokens par document) reste à définir.
Pourquoi je souhaite réaliser ce projet ?
Tout d'abord, je veux grok ce sujet.
Ensuite, je souhaite intégrer un RAG à mon projet sklein-convarchive.
Enfin, je souhaite l'utiliser pour générer des RAG pour divers livres pour permettre à mes agent IA de m'assister avec davantage de rigueur quand je travaille sur des sujets de science sociale.
Repository de ce projet
Dernière page.