Example vault location
#
Points clés
L’ingénierie du contexte, pas la prise de notes. La valeur d’un coffre Obsidian pour l’IA ne réside pas dans les notes elles-mêmes, mais dans la couche de recherche qui les rend interrogeables. Un coffre de 16 000 fichiers sans système de recherche est une base de données en écriture seule. Un coffre de 200 fichiers avec recherche hybride et intégration MCP est une base de connaissances IA. L’infrastructure de recherche est le produit. Les notes sont la matière première.
La recherche hybride surpasse la recherche par mots-clés ou sémantique pure. BM25 détecte les identifiants exacts et les noms de fonctions. La recherche vectorielle détecte les synonymes et les correspondances conceptuelles entre différentes terminologies. Reciprocal Rank Fusion (RRF) fusionne les deux sans nécessiter de calibration des scores. Aucune méthode seule ne couvre les deux modes de défaillance. Les recherches sur le classement de passages MS MARCO confirment ce constat : la recherche hybride surpasse systématiquement chaque méthode prise isolément.1 L’analyse approfondie du retrieveur hybride couvre les mathématiques de RRF, des exemples concrets avec des chiffres réels, l’analyse des modes de défaillance et un calculateur de fusion interactif.
MCP donne aux outils IA un accès direct au coffre. Les serveurs Model Context Protocol (MCP) exposent le système de recherche comme un outil que Claude Code, Codex CLI, Cursor et d’autres outils IA peuvent appeler directement. L’agent interroge le coffre, reçoit des résultats classés avec attribution des sources et utilise le contexte sans charger des fichiers entiers. Le serveur MCP est une fine couche d’abstraction autour du moteur de recherche.
Le local-first signifie zéro coût d’API et une confidentialité totale. L’ensemble de la pile technique s’exécute sur une seule machine : SQLite pour le stockage, Model2Vec pour les embeddings, FTS5 pour la recherche par mots-clés, sqlite-vec pour le KNN vectoriel. Aucun service cloud, aucun appel API, aucune dépendance réseau. Les notes personnelles ne quittent jamais la machine. Le ré-embedding complet de 49 746 chunks coûterait environ 0,30 $ aux tarifs API d’OpenAI, mais les coûts réels sont la latence, l’exposition de la vie privée et la dépendance réseau pour un système qui devrait fonctionner hors ligne.2
L’indexation incrémentale maintient le système à jour en moins de 10 secondes. La comparaison des dates de modification des fichiers détecte les changements. Seuls les fichiers modifiés sont re-découpés et ré-embarqués. Une réindexation complète prend environ quatre minutes sur du matériel Apple M-series. Les mises à jour incrémentales sur les modifications d’une journée typique s’exécutent en moins de dix secondes. Le système reste à jour sans intervention manuelle.
L’architecture évolue de 200 à plus de 20 000 notes. La même conception en trois couches (ingestion, recherche, intégration) fonctionne quelle que soit la taille du coffre. Commencez avec une recherche BM25 seule sur un petit coffre. Ajoutez la recherche vectorielle lorsque les collisions de mots-clés deviennent problématiques. Ajoutez la fusion RRF lorsque vous avez besoin à la fois de correspondances exactes et sémantiques. Chaque couche est indépendamment utile et indépendamment supprimable.
Comment utiliser ce guide
Ce guide couvre le système complet. Votre point de départ dépend de votre situation :
| Vous êtes… | Commencez ici | Puis explorez |
|---|---|---|
| Nouveau avec Obsidian + IA | Pourquoi Obsidian pour l’infrastructure IA, Démarrage rapide | Architecture du coffre, Architecture du serveur MCP |
| Coffre existant, souhaitant un accès IA | Architecture du serveur MCP, Intégration Claude Code | Modèles d’embedding, Recherche plein texte |
| Construction d’un système de recherche | Le pipeline de recherche complet, Reciprocal Rank Fusion | Optimisation des performances, Dépannage |
| Contexte équipe ou entreprise | Cadre décisionnel, Patterns de graphe de connaissances | Recettes de workflow développeur, Guide de migration |
Les sections marquées Contrat incluent des détails d’implémentation, des blocs de configuration et des modes de défaillance. Les sections marquées Narratif se concentrent sur les concepts, les décisions d’architecture et le raisonnement derrière les choix de conception. Les sections marquées Recette fournissent des workflows étape par étape.
Pourquoi Obsidian pour l’infrastructure IA
La thèse de ce guide : les coffres Obsidian sont le meilleur substrat pour les bases de connaissances IA personnelles car ils sont local-first, en texte brut, structurés en graphe, et l’utilisateur contrôle chaque couche de la pile.
Ce qu’Obsidian apporte à l’IA que les alternatives n’offrent pas
Des fichiers markdown en texte brut. Chaque note est un fichier .md sur votre système de fichiers. Aucun format propriétaire, aucun export de base de données, aucune API requise pour lire le contenu. Tout outil capable de lire des fichiers peut lire votre coffre. grep, ripgrep, pathlib de Python, SQLite FTS5 — ils fonctionnent tous directement sur les fichiers sources. Lorsque vous construisez un système de recherche, vous indexez des fichiers, pas des réponses API. L’index est toujours cohérent avec la source car la source est le système de fichiers.
Une architecture local-first. Le coffre réside sur votre machine. Pas de serveur, pas de dépendance à la synchronisation cloud, pas de limites de débit API, pas de conditions d’utilisation régissant la façon dont vous traitez votre propre contenu. Vous pouvez embarquer, indexer, découper et rechercher vos notes sans aucun service externe. C’est important pour l’infrastructure IA car le pipeline de recherche s’exécute aussi vite que votre disque le permet, pas aussi vite qu’un point de terminaison API répond. C’est également important pour la confidentialité : les notes personnelles contenant des identifiants, des données de santé, des informations financières et des réflexions privées ne quittent jamais votre machine.
Une structure en graphe grâce aux wiki-links. La syntaxe [[wiki-link]] d’Obsidian crée un graphe orienté à travers les notes. Une note sur l’implémentation OAuth renvoie à des notes sur la rotation des jetons, la gestion des sessions et la sécurité API. La structure en graphe encode des relations entre concepts, organisées par l’humain. Les embeddings vectoriels capturent la similarité sémantique, mais les wiki-links capturent des connexions intentionnelles que l’auteur a établies en réfléchissant au sujet. Le graphe est un signal que les embeddings ne peuvent pas reproduire.
Un écosystème de plugins. Obsidian dispose de plus de 1 800 plugins communautaires. Dataview interroge votre coffre comme une base de données. Templater génère des notes à partir de modèles avec une logique JavaScript. L’intégration Git synchronise votre coffre vers un dépôt. Linter impose la cohérence du formatage. Ces plugins ajoutent de la structure au coffre sans modifier le format texte brut sous-jacent. Le système de recherche indexe la sortie de ces plugins, pas les plugins eux-mêmes.
Plus de 5 millions d’utilisateurs. Obsidian possède une large communauté active produisant des modèles, des workflows, des plugins et de la documentation. Lorsque vous rencontrez un problème d’organisation de coffre ou de configuration de plugin, quelqu’un a probablement documenté une solution. La communauté produit également des outils adjacents à Obsidian : des serveurs MCP, des scripts d’indexation, des pipelines de publication et des wrappers API.
Ce qu’un système de fichiers seul ne vous apporte pas
Un répertoire de fichiers markdown possède l’avantage du texte brut mais manque de trois choses qu’Obsidian ajoute :
-
Des liens bidirectionnels. Obsidian suit automatiquement les backlinks. Lorsque vous créez un lien de la Note A vers la Note B, la Note B affiche que la Note A la référence. Le panneau de graphe visualise les clusters de connexions. Cette conscience bidirectionnelle est une métadonnée qu’un système de fichiers brut ne fournit pas.
-
Un aperçu en direct avec rendu des plugins. Les requêtes Dataview, les diagrammes Mermaid et les blocs d’appel se rendent en temps réel. L’expérience d’écriture est plus riche qu’un éditeur de texte tandis que le format de stockage reste du texte brut. Vous écrivez et organisez dans un environnement riche ; le système de recherche indexe le markdown brut.
-
Une infrastructure communautaire. Découverte de plugins, marketplace de thèmes, service de synchronisation (optionnel), service de publication (optionnel) et un écosystème de documentation. Vous pouvez reproduire n’importe quelle fonctionnalité individuelle avec des outils autonomes, mais Obsidian les regroupe dans un workflow cohérent.
Ce qu’Obsidian ne fait PAS (et ce que vous construisez)
Obsidian n’inclut pas d’infrastructure de recherche. Il dispose d’une recherche basique (plein texte, nom de fichier, tag) mais pas de pipeline d’embedding, pas de recherche vectorielle, pas de classement par fusion, pas de serveur MCP, pas de filtrage des identifiants, pas de stratégie de découpage (chunking), et pas de hooks d’intégration pour les outils IA externes. Ce guide couvre l’infrastructure que vous construisez au-dessus d’Obsidian. Le coffre est le substrat. Le pipeline de recherche, le serveur MCP et les hooks d’intégration sont l’infrastructure.
L’architecture décrite ici est markdown-first, pas exclusive à Obsidian. Si vous utilisez Logseq, Foam, Dendron ou un simple répertoire de fichiers markdown, le pipeline de recherche fonctionne de manière identique. Le découpeur lit les fichiers .md. L’encodeur traite des chaînes de texte. L’indexeur écrit dans SQLite. Aucun de ces composants ne dépend de fonctionnalités spécifiques à Obsidian. La contribution d’Obsidian est l’environnement d’écriture et d’organisation qui produit les fichiers markdown que le système de recherche indexe.
Démarrage rapide : votre premier coffre-fort connecté à l’IA
Cette section vous permet de connecter un coffre-fort à un outil d’IA en cinq minutes. Vous allez installer Obsidian, créer un coffre-fort, installer un serveur MCP, et exécuter votre première requête. Ce démarrage rapide utilise un serveur MCP communautaire pour obtenir des résultats immédiats. Les sections suivantes couvrent la construction d’un pipeline de recherche personnalisé pour un usage en production.
Prérequis
- macOS, Linux ou Windows
- Node.js 18+ (pour le serveur MCP)
- Claude Code, Codex CLI, ou Cursor installé
Étape 1 : Créer un coffre-fort
Téléchargez Obsidian depuis obsidian.md et créez un nouveau coffre-fort. Choisissez un emplacement dont vous vous souviendrez — le serveur MCP a besoin du chemin absolu.
# Example vault location
~/Documents/knowledge-base/
Ajoutez quelques notes pour donner au système de recherche du contenu exploitable. Même 10 à 20 notes suffisent pour observer des résultats. Chaque note doit être un fichier .md avec un titre significatif et au moins un paragraphe de contenu.
Étape 2 : Installer un serveur MCP
Plusieurs serveurs MCP communautaires offrent un accès immédiat au coffre-fort. L’écosystème s’est considérablement développé entre 2025 et 2026 :
| Serveur | Auteur | Transport | Nécessite un plugin | Fonctionnalité clé |
|---|---|---|---|---|
| obsidian-mcp-server | StevenStavrakis | STDIO | Non | Léger, basé sur les fichiers |
| mcp-obsidian | MarkusPfundstein | STDIO | Local REST API | CRUD complet du coffre-fort via REST |
| obsidian-mcp-tools | jacksteamdev | STDIO | Oui (plugin) | Recherche sémantique + Templater |
| obsidian-claude-code-mcp | iansinnott | WebSocket | Oui (plugin) | Auto-découverte pour Claude Code |
| obsidian-mcp-server | cyanheads | STDIO | Local REST API | Tags, gestion du frontmatter |
Pour ce démarrage rapide, l’option la plus simple est un serveur basé sur les fichiers qui lit directement les fichiers .md :
npm install -g obsidian-mcp-server
Étape 3 : Configurer votre outil d’IA
Claude Code — ajoutez dans ~/.claude/settings.json :
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mcp-server",
"args": ["--vault", "/absolute/path/to/your/vault"]
}
}
}
Codex CLI — ajoutez dans .codex/config.toml :
[mcp_servers.obsidian]
command = "obsidian-mcp-server"
args = ["--vault", "/absolute/path/to/your/vault"]
Cursor — ajoutez dans .cursor/mcp.json :
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mcp-server",
"args": ["--vault", "/absolute/path/to/your/vault"]
}
}
}
Étape 4 : Exécuter votre première requête
Ouvrez votre outil d’IA et posez une question à laquelle vos notes de coffre-fort peuvent répondre :
Search my Obsidian vault for notes about [topic you wrote about]
L’outil d’IA appelle le serveur MCP, qui effectue une recherche dans votre coffre-fort et renvoie le contenu correspondant. Vous devriez voir des résultats avec les chemins de fichiers et des extraits pertinents.
Ce que vous venez de construire
Vous avez connecté une base de connaissances locale à un outil d’IA via un protocole standard. Le serveur MCP lit les fichiers de votre coffre-fort, effectue une recherche basique et renvoie les résultats. Il s’agit de la version minimale viable.
Ce que ce démarrage rapide ne vous apporte PAS : - La recherche hybride (BM25 + recherche vectorielle + fusion RRF) - La recherche sémantique par embeddings - Le filtrage des identifiants sensibles - L’indexation incrémentale - L’injection automatique de contexte via des hooks
Le reste de ce guide couvre la construction de chacune de ces fonctionnalités. Le démarrage rapide valide le concept. Le pipeline complet offre une recherche de qualité production.
Cadre de décision : Obsidian face aux alternatives
Tous les cas d’usage ne nécessitent pas Obsidian. Cette section identifie quand Obsidian est le bon substrat, quand c’est excessif, et quand une autre solution convient mieux.
Arbre de décision
START: What is your primary content type?
│
├─ Structured data (tables, records, schemas)
│ → Use a database. SQLite, PostgreSQL, or a spreadsheet.
│ → Obsidian is for prose, not tabular data.
│
├─ Ephemeral context (current project, temporary notes)
│ → Use CLAUDE.md / AGENTS.md in the project repo.
│ → These travel with the code and reset per project.
│
├─ Team wiki (shared documentation, onboarding)
│ → Evaluate Notion, Confluence, or a shared git repo.
│ → Obsidian vaults are personal-first. Team sync is possible
│ but not native.
│
└─ Growing personal knowledge corpus
│
├─ < 50 notes
│ → A folder of markdown files + grep is sufficient.
│ → Obsidian adds value mainly through the link graph,
│ which needs density to be useful.
│
├─ 50 - 500 notes
│ → Obsidian adds value. Wiki-links create a navigable graph.
│ → BM25-only search (FTS5) is sufficient at this scale.
│ → Skip vector search and RRF until keyword collisions appear.
│
├─ 500 - 5,000 notes
│ → Full hybrid retrieval becomes valuable. Keyword collisions
│ increase. Semantic search catches queries that BM25 misses.
│ → Add vector search + RRF fusion at this scale.
│
└─ 5,000+ notes
→ Full pipeline is essential. BM25-only returns too much noise.
→ Credential filtering becomes critical (more notes = more
accidentally pasted secrets).
→ Incremental indexing matters (full reindex takes minutes).
→ MCP integration pays dividends on every AI interaction.
Matrice de comparaison
| Critère | Obsidian | Notion | Apple Notes | Système de fichiers | CLAUDE.md |
|---|---|---|---|---|---|
| Local d’abord | Oui | Non (cloud) | Partiel (iCloud) | Oui | Oui |
| Texte brut | Oui (markdown) | Non (blocs) | Non (propriétaire) | Oui | Oui |
| Structure en graphe | Oui (wiki-links) | Partiel (mentions) | Non | Non | Non |
| Indexable par l’IA | Accès direct aux fichiers | API requise | Export nécessaire | Accès direct aux fichiers | Déjà dans le contexte |
| Écosystème de plugins | 1 800+ plugins | Intégrations | Aucun | N/A | N/A |
| Utilisable hors ligne | Complet | Lecture seule en cache | Partiel | Complet | Complet |
| Supporte 10 000+ notes | Oui | Oui (avec API) | Se dégrade | Oui | Non (fichier unique) |
| Coût | Gratuit (core) | 10 $/mois+ | Gratuit | Gratuit | Gratuit |
Quand Obsidian est excessif
- Contexte limité à un seul projet. Si l’IA n’a besoin que du contexte du code en cours, placez-le dans
CLAUDE.md,AGENTS.md, ou la documentation au niveau du projet. Ces fichiers accompagnent le dépôt et sont chargés automatiquement. - Données structurées. Si le contenu est constitué de tableaux, d’enregistrements ou de schémas, utilisez une base de données. Les notes Obsidian sont conçues pour la prose. Dataview peut interroger les champs du frontmatter, mais une vraie base de données gère mieux les requêtes structurées.
- Recherche temporaire. Si les notes seront supprimées à la fin du projet, un répertoire de travail avec des fichiers markdown est plus simple. Ne construisez pas une infrastructure de recherche pour du contenu éphémère.
Quand Obsidian est le bon choix
- Accumulation de connaissances sur des mois ou des années. La valeur se compose à mesure que le corpus grandit. Un coffre-fort de 200 notes interrogé quotidiennement pendant six mois apporte plus de valeur qu’un coffre-fort de 5 000 notes interrogé une seule fois.
- Plusieurs domaines dans un même corpus. Un coffre-fort contenant des notes sur la programmation, l’architecture, la sécurité, le design et des projets personnels bénéficie d’une recherche inter-domaines qu’un
CLAUDE.mdspécifique à un projet ne peut pas offrir. - Contenu sensible en matière de confidentialité. Le fonctionnement local signifie que le pipeline de recherche n’envoie jamais de contenu vers des services externes. Le coffre-fort contient tout ce que vous y mettez, y compris du contenu que vous ne téléverseriez pas sur un service cloud.
Modèle mental : trois couches
Le système comporte trois couches qui fonctionnent indépendamment mais dont les effets se multiplient lorsqu’elles sont combinées. Chaque couche a une préoccupation différente et un mode de défaillance différent.
┌─────────────────────────────────────────────────────┐
│ INTEGRATION LAYER │
│ MCP servers, hooks, skills, context injection │
│ Concern: delivering context to AI tools │
│ Failure: wrong context, too much context, stale │
└──────────────────────┬──────────────────────────────┘
│ query + ranked results
┌──────────────────────┴──────────────────────────────┐
│ RETRIEVAL LAYER │
│ BM25, vector KNN, RRF fusion, token budget │
│ Concern: finding the right content for any query │
│ Failure: wrong ranking, missed results, slow queries │
└──────────────────────┬──────────────────────────────┘
│ chunked, embedded, indexed
┌──────────────────────┴──────────────────────────────┐
│ INTAKE LAYER │
│ Note creation, signal triage, vault organization │
│ Concern: what enters the vault and how it's stored │
│ Failure: noise, duplicates, missing structure │
└─────────────────────────────────────────────────────┘
L’ingestion détermine ce qui entre dans le coffre-fort. Sans curation, le coffre-fort accumule du bruit : captures d’écran de tweets, articles copiés-collés sans annotation, réflexions inachevées sans contexte. La couche d’ingestion est responsable du contrôle qualité au point d’entrée. Un pipeline de scoring, une convention de balisage ou un processus de révision manuelle — tout mécanisme qui garantit que le coffre-fort contient du contenu qui mérite d’être retrouvé.
La recherche rend le coffre-fort interrogeable. C’est le moteur : découpage des notes en unités de recherche, transformation des segments en vecteurs (embeddings), indexation pour la recherche par mots-clés et sémantique, fusion des résultats avec RRF. La couche de recherche transforme un répertoire de fichiers en une base de connaissances interrogeable. Sans cette couche, le coffre-fort est navigable par exploration manuelle et recherche basique, mais n’est pas accessible programmatiquement par les outils d’IA.
L’intégration connecte la couche de recherche aux outils d’IA. Un serveur MCP expose la recherche comme un outil appelable. Les hooks injectent du contexte automatiquement. Les skills capturent de nouvelles connaissances dans le coffre-fort. La couche d’intégration est l’interface entre la base de connaissances et les agents d’IA qui la consomment.
Les couches sont découplées par conception. Le pipeline de scoring d’ingestion ne sait rien des embeddings. Le moteur de recherche ne sait rien des règles de routage des signaux. Le serveur MCP ne sait rien de la manière dont les notes ont été créées. Ce découplage signifie que vous pouvez améliorer chaque couche indépendamment. Remplacez le modèle d’embedding sans modifier le pipeline d’ingestion. Ajoutez une nouvelle capacité MCP sans modifier le moteur de recherche. Changez les heuristiques de scoring des signaux sans toucher à l’index.
Architecture du coffre-fort pour la consommation par l’IA
Un coffre-fort optimisé pour la recherche par IA suit des conventions différentes de celles d’un coffre-fort optimisé pour la navigation personnelle. Cette section couvre la structure des dossiers, le schéma des notes, les conventions de frontmatter et les patterns spécifiques qui améliorent la qualité de la recherche.
Structure des dossiers
Utilisez des préfixes numérotés pour les dossiers de premier niveau afin de créer une hiérarchie organisationnelle prévisible. Les numéros n’impliquent pas de priorité — ils regroupent des domaines liés et rendent la structure facile à parcourir.
vault/
├── 00-inbox/ # Unsorted captures, pending triage
├── 01-projects/ # Active project notes
├── 02-areas/ # Ongoing areas of responsibility
├── 03-resources/ # Reference material by topic
│ ├── programming/
│ ├── security/
│ ├── ai-engineering/
│ ├── design/
│ └── devops/
├── 04-archive/ # Completed projects, old references
├── 05-signals/ # Scored signal intake
│ ├── ai-tooling/
│ ├── security/
│ ├── systems/
│ └── ...12 domain folders
├── 06-daily/ # Daily notes (if used)
├── 07-templates/ # Note templates (excluded from index)
├── 08-attachments/ # Images, PDFs (excluded from index)
├── .obsidian/ # Obsidian config (excluded from index)
└── .indexignore # Paths to exclude from retrieval index
Dossiers à indexer : tout ce qui contient de la prose en markdown — projets, domaines de responsabilité, ressources, signaux, notes quotidiennes.
Dossiers à exclure de l’indexation : les templates (ils contiennent des variables de substitution, pas du contenu), les pièces jointes (fichiers binaires), la configuration Obsidian, et tout dossier contenant du contenu sensible que vous ne souhaitez pas inclure dans l’index de recherche.
Le fichier .indexignore
Créez un fichier .indexignore à la racine du coffre-fort pour exclure explicitement des chemins de l’index de recherche. La syntaxe est identique à celle de .gitignore :
# Obsidian internal
.obsidian/
# Templates contain placeholders, not content
07-templates/
# Binary attachments
08-attachments/
# Personal health/medical notes
02-areas/health/
# Financial records
02-areas/finance/personal/
# Career documents (resumes, salary data)
02-areas/career/private/
L’indexeur lit ce fichier avant l’analyse et ignore entièrement les chemins correspondants. Les fichiers dans les chemins exclus ne sont jamais découpés, jamais transformés en vecteurs et n’apparaissent jamais dans les résultats de recherche.
Schéma des notes
Chaque note devrait avoir un frontmatter YAML. Le moteur de recherche utilise les champs du frontmatter pour le filtrage et l’enrichissement du contexte :
---
title: "OAuth Token Rotation Patterns"
type: note # note | signal | project | moc | daily
domain: security # primary domain for routing
tags:
- authentication
- oauth
- token-management
created: 2026-01-15
updated: 2026-02-28
source: "" # URL if captured from external source
status: active # active | archived | draft
---
Champs requis pour la recherche :
title— Utilisé dans l’affichage des résultats de recherche et le contexte des en-têtes pour BM25type— Permet des requêtes filtrées par type (« afficher uniquement les MOCs » ou « uniquement les signaux »)tags— Indexés dans le contexte des en-têtes FTS5 avec un poids de 0.3, fournissant des correspondances par mots-clés même lorsque le corps utilise une terminologie différente
Champs optionnels mais précieux :
domain— Permet des requêtes limitées à un domaine (« rechercher uniquement dans les notes de sécurité »)source— Attribution pour le contenu capturé ; le moteur de recherche peut inclure les URLs sources dans les résultatsstatus— Permet d’exclure les notes archivées ou en brouillon de la recherche active
Conventions de découpage
Le moteur de recherche découpe au niveau des en-têtes H2 (##). Cela signifie que la structure de vos notes affecte directement la granularité de la recherche :
Bon pour la recherche :
## Token Rotation Strategy
The rotation interval depends on the threat model...
## Implementation with refresh_token
The OAuth 2.0 refresh token flow requires...
## Error Handling: Expired Tokens
When a token expires mid-request...
Trois sections H2 produisent trois segments interrogeables indépendamment. Chaque segment contient suffisamment de contexte pour que l’embedding capture son sens. Une requête sur « la gestion des tokens expirés » correspond spécifiquement au troisième segment.
Mauvais pour la recherche :
# OAuth Notes
Token rotation depends on threat model. The OAuth 2.0 refresh
token flow requires storing the refresh token securely. When a
token expires mid-request, the client should retry after refresh.
The rotation interval is typically 15-30 minutes for access tokens
and 7-30 days for refresh tokens...
Une longue section sans en-têtes H2 produit un seul segment volumineux. L’embedding fait la moyenne de tous les sujets de la section. Une requête sur n’importe quel sous-thème correspond de manière égale à la note entière.
Règle empirique : si une section couvre plus d’un concept, divisez-la en sous-sections H2. Le découpeur se charge du reste.
Ce qu’il ne faut pas mettre dans les notes
Contenu qui dégrade la qualité de la recherche :
- Copier-coller brut d’articles entiers sans annotation. Le moteur de recherche indexe les mots-clés de l’article original, diluant votre coffre-fort avec du contenu que vous n’avez pas écrit. Ajoutez un résumé, extrayez les points clés ou créez un lien vers l’URL source à la place.
- Captures d’écran sans description textuelle. Le moteur de recherche indexe le texte markdown. Une image sans texte alternatif ni description environnante est invisible tant pour BM25 que pour la recherche vectorielle.
- Chaînes d’identifiants. Clés API, tokens, mots de passe, chaînes de connexion. Même avec le filtrage des identifiants, l’approche la plus sûre est de ne jamais coller de secrets dans les notes. Référencez-les par leur nom (« le token API Cloudflare dans
~/.env») à la place. - Contenu généré automatiquement sans curation. Si un outil génère une note (transcription de réunion, surlignages Readwise, import RSS), relisez-la et annotez-la avant qu’elle n’entre dans le coffre-fort permanent. Les imports automatiques non révisés ajoutent du volume sans ajouter de valeur recherchable.
Écosystème de plugins pour les workflows IA
Les plugins Obsidian qui améliorent la qualité du vault pour la récupération IA se répartissent en trois catégories : structurels (imposent la cohérence), requêtes (exposent les métadonnées) et synchronisation (maintiennent le vault à jour).
Plugins essentiels
Dataview. Interroge votre vault comme une base de données à l’aide des champs frontmatter. Créez des index dynamiques : « toutes les notes avec le tag security mises à jour au cours des 30 derniers jours » ou « toutes les notes de projet avec le statut active ». Dataview n’aide pas directement la récupération, mais il vous permet d’identifier les lacunes dans la couverture de votre vault et de trouver les notes nécessitant une mise à jour.
TABLE type, domain, updated
FROM "03-resources"
WHERE status = "active"
SORT updated DESC
LIMIT 20
Templater. Crée des notes à partir de modèles avec des champs dynamiques. Assurez-vous que chaque nouvelle note commence avec un frontmatter correct en utilisant un modèle qui pré-remplit les champs created, type et domain. Un frontmatter cohérent améliore le filtrage lors de la récupération.
<%* /* New Resource Note Template */ %>
---
title: "<% tp.file.cursor() %>"
type: note
domain: <% tp.system.suggester(["programming", "security", "ai-engineering", "design", "devops"], ["programming", "security", "ai-engineering", "design", "devops"]) %>
tags: []
created: <% tp.date.now("YYYY-MM-DD") %>
updated: <% tp.date.now("YYYY-MM-DD") %>
source: ""
status: active
---
## Key Points
## Details
## References
Linter. Applique des règles de formatage à l’ensemble du vault. Une hiérarchie de titres cohérente (H1 pour le titre, H2 pour les sections, H3 pour les sous-sections) garantit que le chunker produit des résultats prévisibles. Règles du Linter importantes pour la récupération :
- Incrémentation des titres : imposer des niveaux de titre séquentiels (pas de saut de H1 à H3)
- YAML title : correspondre au nom de fichier
- Espaces en fin de ligne : supprimer (évite les artefacts de tokenisation FTS5)
- Lignes vides consécutives : limiter à 1 (chunks plus propres)
Intégration Git. Contrôle de version pour votre vault. Suivez les modifications au fil du temps, synchronisez entre plusieurs machines et récupérez les suppressions accidentelles. Git fournit également les données mtime que l’indexeur utilise pour la détection incrémentale des changements.
Plugins qui facilitent l’indexation
Smart Connections. Un plugin Obsidian qui offre une recherche sémantique alimentée par l’IA directement dans Obsidian. Smart Connections v4 crée des embeddings locaux par défaut — une fois votre vault indexé, les connexions sémantiques et la recherche fonctionnent entièrement hors ligne sans aucun appel API.21 Bien que le système de récupération présenté dans ce guide soit externe à Obsidian (il s’exécute comme un pipeline Python), Smart Connections est utile pour explorer les relations sémantiques pendant la rédaction. Les deux systèmes indexent le même contenu mais répondent à des cas d’usage différents : Smart Connections pour la découverte dans l’éditeur, le récupérateur externe pour l’intégration des outils IA via MCP.
Metadata Menu. Offre une édition structurée du frontmatter avec autocomplétion des valeurs de champs. Réduit les fautes de frappe dans les champs type, domain et tags. Des métadonnées cohérentes améliorent la précision du filtrage lors de la récupération.
Plugins qui nuisent à l’indexation
Excalidraw. Stocke les dessins sous forme de JSON intégré dans des fichiers markdown. Le JSON est syntaxiquement du markdown valide mais produit des résultats incohérents lorsqu’il est découpé en chunks et transformé en embeddings. Excluez les fichiers Excalidraw de l’index via .indexignore ou filtrez par extension de fichier.
Kanban. Stocke l’état du tableau sous forme de markdown spécialement formaté. Le format est conçu pour le rendu Kanban, pas pour la récupération de texte. Le chunker produit des fragments de titres de cartes et de métadonnées qui ne se prêtent pas bien aux embeddings. Excluez les tableaux Kanban de l’index.
Calendar. Crée des notes quotidiennes avec un contenu minimal (souvent juste un en-tête de date). Les notes vides ou quasi vides produisent des chunks de faible qualité. Si vous utilisez des notes quotidiennes, rédigez-y du contenu substantiel ou excluez le dossier des notes quotidiennes de l’index.
Configuration des plugins qui compte
File recovery → Activé. Protège contre la suppression accidentelle de notes. Pas directement lié à la récupération, mais essentiel pour une base de connaissances sur laquelle vous comptez.
Strict line breaks → Désactivé. Les sauts de ligne standard du markdown (double retour à la ligne pour un paragraphe) produisent des chunks plus propres que le mode strict d’Obsidian (simple retour à la ligne pour <br>).
Default new file location → Dossier désigné. Dirigez les nouveaux fichiers vers 00-inbox/ afin que les notes non catégorisées ne polluent pas les dossiers de domaine. La boîte de réception est une zone de transit ; les fichiers sont déplacés vers les dossiers de domaine après le tri.
Wiki-link format → Chemin le plus court si possible. Des cibles de liens plus courtes sont plus faciles à résoudre pour le récupérateur lors de l’indexation de la structure des liens.
Modèles d’embeddings : choix et configuration
Le modèle d’embedding convertit les fragments de texte en vecteurs numériques pour la recherche sémantique. Le choix du modèle détermine la qualité de la recherche, la taille de l’index, la vitesse d’embedding et les dépendances à l’exécution. Cette section explique pourquoi potion-base-8M de Model2Vec est le choix par défaut et dans quels cas privilégier des alternatives.
Pourquoi Model2Vec potion-base-8M
Modèle : minishlab/potion-base-8M
Paramètres : 7,6 millions
Dimensions : 256
Taille : ~30 Mo
Dépendances : model2vec (numpy uniquement, sans PyTorch)
Inférence : CPU uniquement, embeddings statiques de mots (sans couches d’attention)
Model2Vec distille les connaissances d’un sentence transformer en embeddings statiques de tokens. Au lieu d’exécuter des couches d’attention sur l’entrée (comme le font BERT, MiniLM et les autres modèles transformer), Model2Vec produit des vecteurs par moyenne pondérée d’embeddings de tokens précalculés.3 La conséquence pratique : la vitesse d’embedding est 50 à 500 fois supérieure à celle des modèles basés sur des transformers, car il n’y a aucun calcul séquentiel.
Sur la suite de benchmarks MTEB, potion-base-8M atteint 89 % des performances de all-MiniLM-L6-v2 (50,03 contre 56,09 en moyenne).4 L’écart de qualité de 11 % est le compromis accepté en échange des avantages de vitesse et de simplicité. Pour des fragments markdown courts (200 à 400 mots en moyenne dans un coffre-fort typique), la différence de qualité est moins marquée que sur des documents longs, car les deux modèles convergent vers des représentations similaires pour des textes courts et ciblés.
Configuration
# embedder.py
DEFAULT_MODEL = "minishlab/potion-base-8M"
EMBEDDING_DIM = 256
class Model2VecEmbedder:
def __init__(self, model_name=DEFAULT_MODEL):
self._model_name = model_name
self._model = None
def _ensure_model(self):
if self._model is not None:
return
_activate_venv() # Add isolated venv to sys.path
from model2vec import StaticModel
self._model = StaticModel.from_pretrained(self._model_name)
def embed_batch(self, texts):
self._ensure_model()
vecs = self._model.encode(texts)
return [v.tolist() for v in vecs]
Chargement différé. Le modèle se charge à la première utilisation, pas à l’importation. Importer le module d’embedding ne coûte rien lorsque le système de recherche fonctionne en mode de repli BM25 uniquement (par exemple, lorsque l’environnement virtuel d’embedding n’est pas installé).
Environnement virtuel isolé. Le modèle s’exécute dans un venv dédié (par exemple ~/.claude/venvs/memory/) afin d’éviter les conflits de dépendances avec le reste de la chaîne d’outils. La fonction _activate_venv() ajoute le répertoire site-packages du venv au sys.path à l’exécution.
# Create isolated venv
python3 -m venv ~/.claude/venvs/memory
~/.claude/venvs/memory/bin/pip install model2vec
Traitement par lots. L’embedder traite les textes par lots de 64 pour amortir la surcharge de Model2Vec. L’indexeur transmet les fragments à embed_batch() plutôt que de les encoder un par un.
Quand choisir des alternatives
| Modèle | Dim | Taille | Vitesse | Qualité (MTEB) | Idéal pour |
|---|---|---|---|---|---|
| potion-base-8M | 256 | 30 Mo | 500x | 50,03 | Par défaut : local, rapide, sans GPU |
| potion-base-32M | 256 | 120 Mo | 400x | 52,46 | Meilleure qualité, toujours statique |
| potion-retrieval-32M | 256 | 120 Mo | 400x | 36,35 (recherche) | Statique optimisé pour la recherche |
| all-MiniLM-L6-v2 | 384 | 80 Mo | 1x | 56,09 | Meilleure qualité, toujours local |
| nomic-embed-text-v1.5 | 768 | 270 Mo | 0,5x | 62,28 | Meilleure qualité locale |
| text-embedding-3-small | 1536 | API | N/A | 62,30 | Basé sur API, qualité maximale |
Choisissez potion-base-32M si vous souhaitez une meilleure qualité que potion-base-8M sans quitter la famille des embeddings statiques. Publié en janvier 2025, il utilise un vocabulaire plus large distillé à partir de baai/bge-base-en-v1.5, atteignant 52,46 de moyenne MTEB (amélioration de 5 % par rapport à potion-base-8M) tout en conservant la même sortie en 256 dimensions et la dépendance numpy uniquement.18 Le fichier modèle 4 fois plus volumineux augmente l’utilisation mémoire, mais la vitesse d’embedding reste de plusieurs ordres de grandeur supérieure à celle des modèles transformer.
Choisissez potion-retrieval-32M lorsque votre cas d’utilisation principal est la recherche (ce qui est le cas pour la recherche dans un coffre-fort). Cette variante est affinée (fine-tuned) à partir de potion-base-32M spécifiquement pour les tâches de recherche, obtenant 36,35 sur les benchmarks de recherche MTEB contre 33,52 pour le modèle de base.18 La moyenne MTEB globale descend à 49,73 car l’affinage sacrifie les performances générales au profit de gains spécifiques à la recherche.
Choisissez all-MiniLM-L6-v2 lorsque la qualité de recherche prime sur la vitesse et que PyTorch est installé. Les vecteurs en 384 dimensions augmentent la taille de la base SQLite d’environ 50 % par rapport aux vecteurs en 256 dimensions. La vitesse d’embedding passe de moins d’une minute à environ 10 minutes pour une réindexation complète de 15 000 fichiers sur du matériel Apple Silicon (série M).
Choisissez nomic-embed-text-v1.5 lorsque vous avez besoin de la meilleure qualité de recherche locale possible et acceptez une indexation plus lente. Les vecteurs en 768 dimensions triplent approximativement la taille de la base de données. Nécessite PyTorch et un processeur moderne ou un GPU.
Choisissez text-embedding-3-small lorsque la latence réseau et la confidentialité sont des compromis acceptables. API produit les embeddings de la plus haute qualité, mais introduit une dépendance au cloud, un coût par token (0,02 $/million de tokens) et envoie votre contenu aux serveurs d’OpenAI.
Restez sur potion-base-8M dans tous les autres cas. L’avantage de vitesse est crucial pour l’indexation itérative (réindexation pendant le développement), la dépendance numpy uniquement évite la complexité d’installation de PyTorch, et les vecteurs en 256 dimensions maintiennent une base de données compacte.
Quantification et réduction de dimensionnalité
Model2Vec v0.5.0+ prend en charge le chargement de modèles avec une précision et des dimensions réduites.18 Cela s’avère utile pour le déploiement sur du matériel contraint ou pour réduire la taille de la base de données sans changer de modèle :
from model2vec import StaticModel
# Load with int8 quantization (25% of original size)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", quantize=True)
# Load with reduced dimensions (e.g., 128 instead of 256)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", dimensionality=128)
Les modèles quantifiés conservent une qualité de recherche quasi identique pour une fraction de l’empreinte mémoire. La réduction de dimensionnalité suit une troncature de type Matryoshka — les N premières dimensions portent le plus d’information. Passer de 256 à 128 dimensions divise par deux le stockage des vecteurs avec une perte de qualité minimale pour la recherche sur des textes courts.
Depuis mai 2025, Model2Vec prend également en charge les tokenizers BPE et Unigram (en plus de WordPiece), ce qui élargit l’ensemble des sentence transformers pouvant être distillés en modèles statiques.20
Affinage pour des embeddings spécifiques au coffre-fort
Model2Vec v0.4.0+ prend en charge l’entraînement de modèles de classification personnalisés par-dessus les embeddings statiques, et la version 0.7.0 ajoute la quantification du vocabulaire et le pooling configurable pour la distillation.20 Ceci est pertinent pour les coffres-forts au vocabulaire spécialisé (notes médicales, références juridiques, jargon propre à un domaine) où les modèles potion par défaut pourraient ne pas capturer les nuances sémantiques :
from model2vec import StaticModel
from model2vec.train import train_model
# Fine-tune on vault-specific data
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
trained_model = train_model(model, train_texts, train_labels)
trained_model.save_pretrained("./vault-embeddings")
Pour la plupart des coffres-forts, potion-base-8M par défaut produit une qualité de recherche suffisante. L’affinage ne se justifie que lorsque la recherche manque systématiquement des connexions spécifiques au domaine qu’un modèle généraliste ne peut pas capturer.
Suivi du hash du modèle
L’indexeur stocke un hash dérivé du nom du modèle et de la taille du vocabulaire. Si vous changez de modèle d’embedding, l’indexeur détecte l’incompatibilité lors de la prochaine exécution incrémentale et déclenche automatiquement une réindexation complète.
def _compute_model_hash(self):
"""Hash model name + vocab size for compatibility tracking."""
key = f"{self._model_name}:{self._model.vocab_size}"
return hashlib.sha256(key.encode()).hexdigest()[:16]
Cela empêche de mélanger des vecteurs provenant de modèles différents dans la même base de données, ce qui produirait des scores de cosine similarity incohérents.
Modes de défaillance
Échec du téléchargement du modèle. La première exécution télécharge le modèle depuis Hugging Face. Si le téléchargement échoue (problème réseau, pare-feu d’entreprise), le système de recherche bascule en mode BM25 uniquement. Le modèle est mis en cache localement après le premier téléchargement.
Incompatibilité de dimensions. Si vous changez de modèle sans vider la base de données, les vecteurs stockés ont une dimension différente de celle des nouveaux embeddings. L’indexeur détecte cela via le hash du modèle et déclenche une réindexation complète. Si la vérification du hash échoue (modèle personnalisé sans hash approprié), sqlite-vec renverra une erreur sur les requêtes KNN en raison de l’incompatibilité de dimensions.
Pression mémoire sur les grands coffres-forts. Encoder plus de 50 000 fragments en un seul lot peut consommer une quantité significative de mémoire. L’indexeur traite par lots de 64 pour limiter le pic d’utilisation mémoire. Si la mémoire pose toujours problème, réduisez la taille des lots.
Recherche plein texte avec FTS5
L’extension FTS5 de SQLite fournit une recherche plein texte avec classement BM25. FTS5 est le composant de recherche par mots-clés du pipeline de récupération hybride (hybrid retrieval). Cette section couvre la configuration de FTS5, les cas où BM25 excelle et ses modes de défaillance spécifiques.
Table virtuelle FTS5
CREATE VIRTUAL TABLE chunks_fts USING fts5(
chunk_text,
section,
heading_context,
content=chunks,
content_rowid=id
);
Mode content-sync. Le paramètre content=chunks indique à FTS5 de référencer directement la table chunks plutôt que de stocker une copie dupliquée du texte. Cela réduit de moitié l’espace de stockage requis, mais implique que FTS5 doit être synchronisé manuellement lors de l’insertion, de la mise à jour ou de la suppression de fragments (chunks).
Colonnes. Trois colonnes sont indexées :
- chunk_text — Le contenu principal de chaque fragment (poids BM25 : 1.0)
- section — Le texte du titre H2 (poids BM25 : 0.5)
- heading_context — Titre de la note, tags et métadonnées (poids BM25 : 0.3)
Classement BM25
BM25 classe les documents selon la fréquence des termes, la fréquence inverse des documents et la normalisation par longueur de document. La fonction auxiliaire bm25() de FTS5 accepte des poids par colonne :
SELECT
c.id, c.file_path, c.section, c.chunk_text,
bm25(chunks_fts, 1.0, 0.5, 0.3) AS score
FROM chunks_fts
JOIN chunks c ON chunks_fts.rowid = c.id
WHERE chunks_fts MATCH ?
ORDER BY score
LIMIT 30;
Les poids des colonnes (1.0, 0.5, 0.3) signifient :
- Une correspondance de mot-clé dans chunk_text contribue le plus au score
- Une correspondance dans section (titre) contribue moitié moins
- Une correspondance dans heading_context (titre, tags) contribue à hauteur de 30 %
Ces poids sont ajustables. Si votre coffre-fort (vault) contient des titres descriptifs qui prédisent fortement la qualité du contenu, augmentez le poids de section. Si vos tags sont complets et précis, augmentez le poids de heading_context.
Quand BM25 l’emporte
BM25 excelle pour les requêtes contenant des identifiants exacts :
- Noms de fonctions :
_rrf_fuse,embed_batch,get_stale_files - CLI flags :
--incremental,--vault,--model - Clés de configuration :
bm25_weight,max_tokens,batch_size - Messages d’erreur :
SQLITE_LOCKED,ConnectionRefusedError - Termes techniques spécifiques :
PostToolUse,PreToolUse,AGENTS.md
Pour ces requêtes, BM25 trouve la correspondance exacte immédiatement. La recherche vectorielle renverrait du contenu sémantiquement lié mais pourrait classer la correspondance exacte plus bas qu’une discussion conceptuelle.
Quand BM25 échoue
BM25 échoue pour les requêtes qui utilisent une terminologie différente de celle du contenu stocké :
- Requête : « how to handle authentication failures » → Le coffre-fort contient des notes sur « login error recovery » et « session expiration handling ». BM25 ne trouve pas de correspondance car les mots-clés diffèrent.
- Requête : « what is the best way to manage state » → Le coffre-fort contient des notes sur « Redux store patterns » et « context providers ». BM25 échoue car la « gestion d’état » est exprimée à travers des noms de technologies spécifiques.
BM25 échoue également en cas de collision de mots-clés à grande échelle. Dans un coffre-fort de 15 000 fichiers, une recherche sur « configuration » correspond à des centaines de notes car presque chaque note de projet mentionne la configuration. Les résultats sont techniquement corrects mais pratiquement inutiles — le classement ne peut pas déterminer quelle note de « configuration » est pertinente pour la requête en cours.
Tokenizer FTS5
FTS5 utilise le tokenizer unicode61 par défaut, qui gère le texte ASCII et Unicode. Pour les coffres-forts contenant beaucoup de contenu CJK (chinois, japonais, coréen), envisagez le tokenizer trigram :
-- For CJK-heavy vaults
CREATE VIRTUAL TABLE chunks_fts USING fts5(
chunk_text, section, heading_context,
content=chunks, content_rowid=id,
tokenize='trigram'
);
Le tokenizer unicode61 par défaut découpe aux limites de mots, ce qui fonctionne mal pour les langues sans espaces entre les mots. Le tokenizer trigram découpe tous les trois caractères, permettant la recherche de sous-chaînes au prix d’un index plus volumineux (environ 3 fois plus grand).
Maintenance
FTS5 nécessite une synchronisation explicite lorsque la table chunks sous-jacente est modifiée :
# After inserting chunks
cursor.execute("""
INSERT INTO chunks_fts(chunks_fts)
VALUES('rebuild')
""")
La commande rebuild reconstruit l’index FTS5 à partir de la table de contenu. Exécutez-la après des insertions en masse (réindexation complète) mais pas après des mises à jour incrémentales individuelles — pour celles-ci, utilisez INSERT INTO chunks_fts(rowid, chunk_text, section, heading_context) pour synchroniser les lignes individuellement.
Recherche vectorielle avec sqlite-vec
L’extension sqlite-vec apporte la recherche vectorielle KNN (K-Nearest Neighbors) dans SQLite. Cette section couvre la configuration de sqlite-vec, le pipeline d’embeddings de la note au vecteur interrogeable et les schémas de requêtes spécifiques.
Table virtuelle sqlite-vec
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
id INTEGER PRIMARY KEY,
embedding float[256]
);
Le module vec0 stocke des vecteurs flottants à 256 dimensions sous forme de données binaires compactées. La colonne id correspond 1:1 à la table chunks, permettant des jointures entre les résultats vectoriels et les métadonnées des fragments.
Pipeline d’embeddings
Le pipeline transforme une note en vecteur interrogeable :
Note (.md file)
→ Chunker: split at H2 boundaries
→ Chunks (30-2000 chars each)
→ Credential filter: scrub secrets
→ Embedder: Model2Vec encode
→ Vectors (256-dim float arrays)
→ sqlite-vec: store as packed binary
→ Ready for KNN queries
Sérialisation des vecteurs
Le module struct de Python sérialise les vecteurs flottants pour le stockage sqlite-vec :
import struct
def _serialize_vector(vec):
"""Pack float list into binary for sqlite-vec."""
return struct.pack(f"{len(vec)}f", *vec)
def _deserialize_vector(blob, dim=256):
"""Unpack binary blob to float list."""
return list(struct.unpack(f"{dim}f", blob))
Requête KNN
Une requête de recherche vectorielle encode la requête d’entrée sous forme d’embedding, puis trouve les K fragments les plus proches par distance cosinus :
def _vector_search(self, query_text, limit=30):
query_vec = self.embedder.embed_batch([query_text])[0]
packed = _serialize_vector(query_vec)
results = self.db.execute("""
SELECT
cv.id,
cv.distance,
c.file_path,
c.section,
c.chunk_text
FROM chunk_vecs cv
JOIN chunks c ON cv.id = c.id
WHERE embedding MATCH ?
AND k = ?
ORDER BY distance
""", [packed, limit]).fetchall()
return results
L’opérateur MATCH dans sqlite-vec effectue une recherche approximative du plus proche voisin. Le paramètre k contrôle le nombre de résultats à renvoyer. La colonne distance contient la distance cosinus (0 = identique, 2 = opposé).
Quand la recherche vectorielle l’emporte
La recherche vectorielle excelle pour les requêtes où le concept importe plus que les mots spécifiques :
- Requête : « how to handle authentication failures » → Trouve des notes sur « login error recovery » (même espace sémantique, mots-clés différents)
- Requête : « what patterns exist for caching » → Trouve des notes sur « memoization », « Redis TTL strategies » et « HTTP cache headers » (concepts liés, terminologie diverse)
- Requête : « approaches to testing asynchronous code » → Trouve des notes sur « pytest-asyncio fixtures », « mock event loops » et « async test patterns » (même concept exprimé à travers des détails d’implémentation)
Quand la recherche vectorielle échoue
La recherche vectorielle a du mal avec les identifiants exacts :
- Requête :
_rrf_fuse→ Renvoie des notes sur les « algorithmes de fusion » et le « rank merging » mais peut classer la définition réelle de la fonction plus bas que les discussions conceptuelles - Requête :
PostToolUse→ Renvoie des notes sur les « hooks de cycle de vie des outils » et les « gestionnaires post-exécution » plutôt que le nom spécifique du hook
La recherche vectorielle a également du mal avec les données structurées. Les fichiers de configuration JSON, les blocs YAML et les extraits de code produisent des embeddings qui capturent des schémas structurels plutôt que du sens sémantique. Un fichier JSON avec "review": true génère un embedding différent d’une discussion en prose sur la revue de code.
Dégradation gracieuse
Si sqlite-vec ne parvient pas à se charger (extension manquante, plateforme incompatible, bibliothèque corrompue), le système de récupération se rabat sur une recherche BM25 uniquement :
class VectorIndex:
def __init__(self, db_path):
self.db = sqlite3.connect(db_path)
self._vec_available = False
try:
self.db.enable_load_extension(True)
self.db.load_extension("vec0")
self._vec_available = True
except Exception:
pass # BM25-only mode
@property
def vec_available(self):
return self._vec_available
Le système de récupération vérifie vec_available avant de tenter des requêtes vectorielles. Lorsque cette option est désactivée, toutes les recherches utilisent uniquement BM25 et l’étape de fusion RRF est ignorée.
Reciprocal Rank Fusion (RRF)
RRF fusionne deux listes classées sans nécessiter de calibration des scores. Cette section couvre l’algorithme, un exemple détaillé de requête, le réglage du paramètre k, et les raisons pour lesquelles RRF est préféré aux alternatives. Pour un calculateur interactif avec des rangs modifiables, des scénarios prédéfinis et un explorateur visuel de l’architecture, consultez l’analyse approfondie du retriever hybride.
L’algorithme
RRF attribue à chaque document un score basé uniquement sur sa position dans chaque liste :
score(d) = Σ (weight_i / (k + rank_i))
Où :
- k est une constante de lissage (60, selon Cormack et al.1)
- rank_i est le rang du document (indexé à partir de 1) dans la liste de résultats i
- weight_i est un multiplicateur optionnel par liste (par défaut 1.0)
Les documents bien classés dans plusieurs listes obtiennent des scores fusionnés plus élevés. Les documents qui n’apparaissent que dans une seule liste reçoivent un score provenant de cette unique source.
Pourquoi RRF plutôt que les alternatives
La combinaison linéaire pondérée nécessite de calibrer les scores BM25 par rapport aux distances cosinus. Les scores BM25 ne sont pas bornés et varient selon la taille du corpus. Les distances cosinus sont bornées [0, 2]. Les combiner exige une normalisation, et les paramètres de normalisation dépendent du jeu de données. RRF utilise uniquement les positions de rang, qui sont toujours des entiers commençant à 1, indépendamment de la méthode de scoring.
Les modèles de fusion appris nécessitent des données d’entraînement étiquetées — des paires de pertinence requête-document. Pour une base de connaissances personnelle, ces données d’entraînement n’existent pas. Il faudrait évaluer manuellement des centaines de paires requête-document pour entraîner un modèle utile. RRF fonctionne sans aucune donnée d’entraînement.
Les méthodes de vote Condorcet (comptage de Borda, méthode de Schulze) sont théoriquement élégantes mais plus complexes à implémenter et à régler. L’article original sur RRF a démontré que RRF surpasse les méthodes Condorcet sur les données d’évaluation TREC.1
La fusion en pratique
Requête : « how does the review aggregator handle disagreements »
BM25 classe review-aggregator.py en position 3 (correspondances exactes de mots-clés sur « review », « aggregator », « disagreements ») mais place deux fichiers de configuration plus haut (ils correspondent davantage à « review »). La recherche vectorielle classe le même chunk en position 1 (correspondance sémantique sur la résolution de conflits). Après la fusion RRF :
| Chunk | BM25 | Vec | Score fusionné |
|---|---|---|---|
| review-aggregator.py “Disagreement Resolution” | #3 | #1 | 0.0323 |
| code-review-patterns.md “Multi-Reviewer” | #4 | #2 | 0.0317 |
| deliberation-config.json “Review Weights” | #1 | — | 0.0164 |
Les chunks bien classés dans les deux listes remontent en tête. Les chunks qui n’apparaissent que dans une seule liste obtiennent un score provenant d’une source unique et se retrouvent en dessous des résultats classés dans les deux listes. La logique de résolution des désaccords l’emporte parce que les deux méthodes l’ont trouvée — BM25 grâce aux mots-clés, la recherche vectorielle grâce à la sémantique.
Pour le détail complet étape par étape avec le calcul RRF par rang, essayez différentes valeurs de k dans le calculateur interactif RRF.
Implémentation
RRF_K = 60
def _rrf_fuse(self, bm25_results, vec_results,
bm25_weight=1.0, vec_weight=1.0):
"""Fuse BM25 and vector results using Reciprocal Rank Fusion."""
scores = {}
for rank, r in enumerate(bm25_results, start=1):
cid = r["id"]
if cid not in scores:
scores[cid] = {
"rrf_score": 0.0,
"file_path": r["file_path"],
"section": r["section"],
"chunk_text": r["chunk_text"],
"bm25_rank": None,
"vec_rank": None,
}
scores[cid]["rrf_score"] += bm25_weight / (self._rrf_k + rank)
scores[cid]["bm25_rank"] = rank
for rank, r in enumerate(vec_results, start=1):
cid = r["id"]
if cid not in scores:
scores[cid] = {
"rrf_score": 0.0,
"file_path": r["file_path"],
"section": r["section"],
"chunk_text": r["chunk_text"],
"bm25_rank": None,
"vec_rank": None,
}
scores[cid]["rrf_score"] += vec_weight / (self._rrf_k + rank)
scores[cid]["vec_rank"] = rank
fused = sorted(
scores.values(),
key=lambda x: x["rrf_score"],
reverse=True,
)
return fused
Réglage de k
La constante k contrôle le poids accordé aux résultats les mieux classés par rapport aux résultats moins bien classés :
- k faible (ex. 10) : Les résultats les mieux classés dominent. Le rang 1 obtient 1/11 = 0,091, le rang 10 obtient 1/20 = 0,050 (différence de 1,8x). Adapté lorsque vous faites confiance aux classements individuels pour identifier correctement le meilleur résultat.
- k par défaut (60) : Équilibré. Le rang 1 obtient 1/61 = 0,0164, le rang 10 obtient 1/70 = 0,0143 (différence de 1,15x). Les écarts de rang sont compressés, ce qui accorde plus de poids au fait d’apparaître dans plusieurs listes.
- k élevé (ex. 200) : Apparaître dans les deux listes compte bien plus que la position de rang. Le rang 1 obtient 1/201, le rang 10 obtient 1/210 — quasi identiques. À utiliser lorsque les classements individuels sont bruités mais que l’accord entre les listes est fiable.
Commencez avec k=60. L’article original sur RRF a démontré la robustesse de cette valeur sur des jeux de données TREC variés. Ne modifiez ce paramètre qu’après avoir mesuré les cas d’échec sur votre propre distribution de requêtes.
Résolution des égalités
Lorsque deux chunks ont des scores RRF identiques (rare mais possible avec le même rang dans une liste et aucune apparition dans l’autre), résolvez les égalités selon l’ordre suivant :
- Préférez les chunks qui apparaissent dans les deux listes à ceux qui n’apparaissent que dans une seule
- Parmi les chunks présents dans les deux listes, préférez celui dont le rang combiné est le plus bas
- Parmi les chunks présents dans une seule liste, préférez celui dont le rang est le plus bas dans cette liste
Le pipeline de recherche complet
Cette section retrace une requête de l’entrée à la sortie à travers l’ensemble du pipeline : recherche BM25, recherche vectorielle, fusion RRF, troncature par budget de tokens et assemblage du contexte.
Flux de bout en bout
User query: "PostToolUse hook for context compression"
│
├─ BM25 Search (FTS5)
│ → MATCH "PostToolUse hook context compression"
│ → Top 30 results ranked by BM25 score
│ → 12ms
│
├─ Vector Search (sqlite-vec)
│ → Embed query with Model2Vec
│ → KNN k=30 on chunk_vecs
│ → Top 30 results ranked by cosine distance
│ → 8ms
│
└─ RRF Fusion
→ Merge 60 candidates (may overlap)
→ Score by rank position
→ Top 10 results
→ 3ms
│
└─ Token Budget
→ Truncate to max_tokens (default 4000)
→ Estimate at 4 chars per token
→ Return results with metadata
→ <1ms
Latence totale : ~23 ms pour une base de données de 49 746 chunks sur du matériel Apple M3 Pro.
La API de recherche
class HybridRetriever:
def search(self, query, limit=10, max_tokens=4000,
bm25_weight=1.0, vec_weight=1.0):
"""
Search the vault using hybrid BM25 + vector retrieval.
Args:
query: Search query text
limit: Maximum results to return
max_tokens: Token budget for total result text
bm25_weight: Weight for BM25 results in RRF
vec_weight: Weight for vector results in RRF
Returns:
List of SearchResult with file_path, section,
chunk_text, rrf_score, bm25_rank, vec_rank
"""
# BM25 search
bm25_results = self._bm25_search(query, limit=30)
# Vector search (if available)
if self.index.vec_available:
vec_results = self._vector_search(query, limit=30)
fused = self._rrf_fuse(
bm25_results, vec_results,
bm25_weight, vec_weight,
)
else:
fused = bm25_results # BM25-only fallback
# Token budget truncation
results = []
token_count = 0
for r in fused[:limit]:
chunk_tokens = len(r["chunk_text"]) // 4
if token_count + chunk_tokens > max_tokens:
break
results.append(r)
token_count += chunk_tokens
return results
Troncature par budget de tokens
Le paramètre max_tokens empêche le système de recherche de renvoyer plus de contexte que ce que l’outil IA peut exploiter. L’estimation utilise 4 caractères par token (une approximation raisonnable pour la prose anglaise). Les résultats sont tronqués de manière gloutonne : on ajoute les résultats par ordre de classement jusqu’à épuisement du budget.
Il s’agit d’une stratégie conservatrice. Une approche plus sophistiquée prendrait en compte les scores de qualité par résultat et privilégierait les résultats plus courts et de meilleure qualité par rapport aux résultats plus longs et de moindre qualité. L’approche gloutonne est plus simple et fonctionne bien en pratique, car le classement RRF ordonne déjà les résultats par pertinence.
Schéma de la base de données (complet)
-- Chunk content and metadata
CREATE TABLE chunks (
id INTEGER PRIMARY KEY,
file_path TEXT NOT NULL,
section TEXT NOT NULL,
chunk_text TEXT NOT NULL,
heading_context TEXT DEFAULT '',
mtime_ns INTEGER NOT NULL,
embedded_at REAL NOT NULL
);
CREATE INDEX idx_chunks_file ON chunks(file_path);
CREATE INDEX idx_chunks_mtime ON chunks(mtime_ns);
-- FTS5 for BM25 search (content-synced to chunks table)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
chunk_text, section, heading_context,
content=chunks, content_rowid=id
);
-- sqlite-vec for vector KNN search
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
id INTEGER PRIMARY KEY,
embedding float[256]
);
-- Model metadata for compatibility tracking
CREATE TABLE model_meta (
key TEXT PRIMARY KEY,
value TEXT
);
Chemin de dégradation progressive
Full pipeline: BM25 + Vector + RRF → Best results
No sqlite-vec: BM25 only → Good results (no semantic)
No model download: BM25 only → Good results (no semantic)
No FTS5: Vector only → Decent results (no keyword)
No database: Error → Prompt user to run indexer
Le système de recherche vérifie les capacités disponibles à l’initialisation et adapte sa stratégie de requête en conséquence. Un composant manquant dégrade la qualité mais ne provoque pas d’erreur. Le seul échec critique est l’absence du fichier de base de données.
Statistiques en production
Mesures effectuées sur un coffre-fort de 16 894 fichiers, 49 746 chunks, base de données SQLite de 83 Mo, Apple M3 Pro :
| Métrique | Valeur |
|---|---|
| Total de fichiers | 16 894 |
| Total de chunks | 49 746 |
| Taille de la base de données | 83 Mo |
| Latence de requête BM25 (p50) | 12 ms |
| Latence de requête vectorielle (p50) | 8 ms |
| Latence de fusion RRF | 3 ms |
| Latence de recherche de bout en bout (p50) | 23 ms |
| Temps de réindexation complète | ~4 minutes |
| Temps de réindexation incrémentale | <10 secondes |
| Modèle d’embeddings | potion-base-8M (256-dim) |
| Pool de candidats BM25 | 30 |
| Pool de candidats vectoriels | 30 |
| Limite de résultats par défaut | 10 |
| Budget de tokens par défaut | 4 000 tokens |
Hachage du contenu et détection des modifications
L’indexeur doit savoir quels fichiers ont changé depuis la dernière exécution de l’indexation. Cette section couvre le mécanisme de détection des modifications et la stratégie de hachage.
Comparaison des dates de modification des fichiers
L’indexeur stocke mtime_ns (date de modification du fichier en nanosecondes) pour chaque chunk dans la table chunks. Lors d’une exécution incrémentale, l’indexeur :
- Parcourt le coffre-fort à la recherche de tous les fichiers
.mddans les dossiers autorisés - Lit le
mtime_nsde chaque fichier depuis le système de fichiers - Compare avec le
mtime_nsstocké dans la base de données - Identifie trois catégories :
- Nouveaux fichiers : le chemin existe dans le système de fichiers mais pas dans la base de données
- Fichiers modifiés : le chemin existe dans les deux mais le
mtime_nsdiffère - Fichiers supprimés : le chemin existe dans la base de données mais pas dans le système de fichiers
def get_stale_files(self, vault_mtimes):
"""Find files whose mtime changed or are new."""
stored = dict(self.db.execute(
"SELECT DISTINCT file_path, mtime_ns FROM chunks"
).fetchall())
stale = []
for path, mtime in vault_mtimes.items():
if path not in stored or stored[path] != mtime:
stale.append(path)
return stale
def get_deleted_files(self, vault_paths):
"""Find files in database that no longer exist in vault."""
stored_paths = set(r[0] for r in self.db.execute(
"SELECT DISTINCT file_path FROM chunks"
).fetchall())
return stored_paths - set(vault_paths)
Pourquoi mtime plutôt qu’un hash du contenu
Le hachage du contenu (SHA-256 du contenu du fichier) serait plus fiable que la comparaison de mtime — il détecterait les cas où un fichier a été touché sans être modifié (par exemple, un git checkout restaurant le mtime d’origine). Cependant, le hachage nécessite de lire chaque fichier à chaque exécution incrémentale. Pour 16 894 fichiers, la lecture du contenu prend 2 à 3 secondes. La lecture des mtimes depuis le système de fichiers prend moins de 100 ms.
Le compromis : la comparaison de mtime déclenche parfois une réindexation inutile de fichiers inchangés (faux positifs), mais ne manque jamais les modifications réelles. Les faux positifs coûtent quelques appels d’embeddings supplémentaires par exécution. La différence de vitesse (100 ms contre 3 secondes) fait de mtime le choix pragmatique pour un système qui s’exécute à chaque interaction avec l’IA.
Gestion des suppressions
Lorsqu’un fichier est supprimé du coffre-fort, l’indexeur retire tous ses chunks de la base de données :
def remove_file(self, file_path):
"""Remove all chunks and vectors for a file."""
chunk_ids = [r[0] for r in self.db.execute(
"SELECT id FROM chunks WHERE file_path = ?",
[file_path],
).fetchall()]
for cid in chunk_ids:
self.db.execute(
"DELETE FROM chunk_vecs WHERE id = ?", [cid]
)
self.db.execute(
"DELETE FROM chunks WHERE file_path = ?",
[file_path],
)
Les tables FTS5 synchronisées par contenu nécessitent une suppression explicite via INSERT INTO chunks_fts(chunks_fts, rowid, ...) VALUES('delete', ?, ...) pour chaque ligne retirée. L’indexeur gère cette opération dans le cadre du processus de suppression de fichier.
Réindexation incrémentale vs complète
L’indexeur prend en charge deux modes : incrémental (rapide, usage quotidien) et complet (lent, occasionnel). Cette section explique quand utiliser chacun, les garanties d’idempotence et la récupération après corruption.
Réindexation incrémentale
Quand l’utiliser : indexation quotidienne après la modification de notes. C’est le mode par défaut.
Ce qu’il fait : 1. Analyse le coffre-fort à la recherche de fichiers modifiés (comparaison des mtime) 2. Supprime les fragments des fichiers supprimés 3. Redécoupe et recalcule les embeddings des fichiers modifiés 4. Insère de nouveaux fragments pour les nouveaux fichiers 5. Synchronise l’index FTS5
Durée typique : moins de 10 secondes pour les modifications d’une journée sur un coffre-fort de 16 000 fichiers.
python index_vault.py --incremental
Réindexation complète
Quand l’utiliser : - Après un changement de modèle d’embedding (détection d’une divergence du hash du modèle) - Après une migration de schéma (nouvelles colonnes, index modifiés) - Après une corruption de la base de données (l’intégrité échoue à la vérification) - Lorsque l’indexation incrémentale produit des résultats inattendus
Ce qu’il fait : 1. Supprime toutes les données existantes (fragments, vecteurs, entrées FTS5) 2. Analyse l’intégralité du coffre-fort 3. Découpe tous les fichiers en fragments 4. Calcule les embeddings de tous les fragments 5. Reconstruit l’index FTS5 depuis zéro
Durée typique : environ 4 minutes pour 16 894 fichiers sur Apple M3 Pro.
python index_vault.py --full
Idempotence
Les deux modes sont idempotents : exécuter la même commande deux fois produit le même résultat. L’indexeur supprime les fragments existants d’un fichier avant d’en insérer de nouveaux, de sorte qu’une réexécution de l’indexation incrémentale sur une base de données déjà à jour ne produit aucun changement. Une réexécution de l’indexation complète produit une base de données identique.
Récupération après corruption
Si la base de données SQLite est corrompue (coupure de courant pendant une écriture, erreur disque, processus interrompu en cours de transaction) :
# Check integrity
sqlite3 vectors.db "PRAGMA integrity_check;"
# If corruption detected, full reindex rebuilds from source files
python index_vault.py --full
La source de vérité est toujours constituée par les fichiers du coffre-fort, pas par la base de données. La base de données est un artefact dérivé qui peut être reconstruit à tout moment. C’est une propriété de conception essentielle : vous n’avez jamais besoin de sauvegarder la base de données.
L’option --incremental
Lorsque l’indexeur s’exécute avec --incremental :
- Vérification du hash du modèle. Compare le hash du modèle stocké avec le modèle actuel. En cas de divergence, bascule automatiquement en mode de réindexation complète et avertit l’utilisateur.
- Analyse des fichiers. Parcourt les dossiers autorisés, collecte les chemins de fichiers et les mtimes.
- Détection des changements. Compare avec les données stockées.
- Traitement par lots. Redécoupe et recalcule les embeddings des fichiers modifiés par lots de 64.
- Rapport de progression. Affiche le nombre de fichiers traités et le temps écoulé.
- Arrêt gracieux. Gère le signal SIGINT en terminant le fichier en cours avant de s’arrêter.
Filtrage des identifiants et limites des données
Les notes personnelles contiennent des secrets : clés API, jetons bearer, chaînes de connexion aux bases de données, clés privées collées lors de sessions de débogage. Le filtre d’identifiants empêche ces éléments d’entrer dans l’index de recherche.
Le problème
Une note sur le débogage d’une intégration OAuth pourrait contenir :
The token was: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
I used this curl command:
curl -H "Authorization: Bearer sk-ant-api03-abc123..."
Sans filtrage, le JWT et la clé API seraient découpés en fragments, transformés en embeddings et stockés dans la base de données. Une recherche sur « authentification » renverrait le fragment contenant de véritables secrets. Pire encore, si le système de recherche transmet les résultats à un outil d’IA via MCP, les secrets apparaissent dans la fenêtre de contexte de l’IA et potentiellement dans les journaux de l’outil.
Filtrage par motifs
Le filtre d’identifiants s’exécute sur chaque fragment avant stockage, en utilisant 25 motifs spécifiques aux fournisseurs ainsi que des motifs génériques :
Motifs spécifiques aux fournisseurs :
| Motif | Exemple | Regex |
|---|---|---|
| Clé API OpenAI | sk-... |
sk-[a-zA-Z0-9_-]{20,} |
| Clé API Anthropic | sk-ant-api03-... |
sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,} |
| PAT GitHub | ghp_... |
gh[ps]_[a-zA-Z0-9]{36,} |
| Clé d’accès AWS | AKIA... |
AKIA[0-9A-Z]{16} |
| Clé Stripe | sk_live_... |
[sr]k_(live\|test)_[a-zA-Z0-9]{24,} |
| Jeton Cloudflare | ... |
Divers motifs |
Motifs génériques :
| Motif | Détection |
|---|---|
| Jetons JWT | eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+ |
| Jetons Bearer | Bearer\s+[a-zA-Z0-9_\-\.]+ |
| Clés privées | -----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY----- |
| Base64 à haute entropie | Chaînes avec >4,5 bits/caractère d’entropie, 40+ caractères |
| Assignations de mots de passe | password\s*[:=]\s*["'][^"']+["'] |
Implémentation du filtre
def clean_content(text):
"""Scrub credentials from text before indexing."""
result = ScanResult(is_clean=True, match_count=0, patterns=[])
for pattern in CREDENTIAL_PATTERNS:
matches = pattern.regex.findall(text)
if matches:
text = pattern.regex.sub(
f"[REDACTED:{pattern.name}]", text
)
result.is_clean = False
result.match_count += len(matches)
result.patterns.append(pattern.name)
return text, result
Choix de conception clés :
-
Filtrer avant le calcul des embeddings. C’est le texte nettoyé qui est transformé en embedding. La représentation vectorielle n’encode jamais les motifs d’identifiants. Une requête pour « clé API » renvoie les notes qui traitent de la gestion des clés API, pas les notes qui contiennent de véritables clés.
-
Remplacer, pas supprimer. Le jeton
[REDACTED:pattern-name]préserve le contexte sémantique du texte environnant. L’embedding capture le fait que « quelque chose ressemblant à un identifiant se trouvait ici » sans encoder l’identifiant lui-même. -
Journaliser les motifs, pas les valeurs. Le filtre enregistre quels motifs ont été détectés (par exemple, « Scrubbed 2 credential(s) from oauth-debug.md [jwt, bearer-token] ») mais ne journalise jamais la valeur de l’identifiant.
Exclusion par chemin
Le fichier .indexignore offre une exclusion à gros grain par chemin. Le filtre d’identifiants offre un nettoyage à grain fin au sein des fichiers indexés. Les deux sont nécessaires :
.indexignorepour les dossiers entiers dont vous savez qu’ils contiennent du contenu sensible (notes de santé, documents financiers, documents de carrière)- Le filtre d’identifiants pour les secrets accidentellement intégrés dans du contenu par ailleurs indexable
Classification des données
Pour les coffres-forts contenant du contenu diversifié, envisagez de classer les notes par niveau de sensibilité :
| Niveau | Exemples | Indexer ? | Filtrer ? |
|---|---|---|---|
| Public | Brouillons de blog, notes techniques | Oui | Oui |
| Interne | Plans de projet, décisions d’architecture | Oui | Oui |
| Sensible | Données salariales, dossiers médicaux | Non (.indexignore) | N/A |
| Restreint | Identifiants, clés privées | Non (.indexignore) | N/A |
Architecture du serveur MCP
Le protocole Model Context Protocol (MCP) permet d’exposer le système de recherche sous forme d’outil que les agents IA peuvent appeler. Cette section couvre la conception du serveur, la surface de fonctionnalités et les limites de permissions.
Choix du protocole : STDIO vs HTTP
MCP prend en charge deux modes de transport :
STDIO — L’outil IA lance le serveur MCP en tant que processus enfant et communique via stdin/stdout. C’est le mode standard pour les outils locaux. Claude Code, Codex CLI et Cursor prennent tous en charge les serveurs MCP en mode STDIO.
{
"mcpServers": {
"obsidian": {
"command": "python",
"args": ["/path/to/obsidian_mcp.py"],
"env": {
"VAULT_PATH": "/path/to/vault",
"DB_PATH": "/path/to/vectors.db"
}
}
}
}
HTTP — Le serveur MCP fonctionne comme un service HTTP autonome. Utile pour l’accès distant, les configurations multi-clients ou les configurations d’équipe où le coffre se trouve sur un serveur partagé.
{
"mcpServers": {
"obsidian": {
"url": "http://localhost:3333/mcp"
}
}
}
Recommandation : Utilisez STDIO pour les coffres personnels. C’est plus simple, plus sécurisé (aucune exposition réseau) et le cycle de vie du serveur est géré par l’outil IA. N’utilisez HTTP que lorsque plusieurs outils ou plusieurs machines nécessitent un accès concurrent au même coffre.
Évolution de la spécification MCP. La spécification MCP de juin 2025 a ajouté l’autorisation OAuth 2.1, les sorties d’outils structurées (schémas de retour typés) et l’élicitation (invites utilisateur initiées par le serveur). La version de novembre 2025 a introduit le transport Streamable HTTP en tant que mode de transport de première classe, la découverte d’URL
.well-knownpour la navigation automatique des capacités du serveur, les annotations d’outils structurées qui déclarent si un outil est en lecture seule ou mutatif, et un système de standardisation des niveaux SDK.1619 La prochaine version de la spécification (provisoirement mi-2026) propose des opérations asynchrones pour les tâches de longue durée, des extensions de protocole spécifiques à certains domaines pour des secteurs comme la santé et la finance, ainsi que des standards de communication agent-à-agent pour les workflows multi-agents.19 Pour les serveurs de coffre personnels, STDIO reste le chemin le plus simple. Le transport Streamable HTTP et la découverte.well-knownbénéficient principalement aux déploiements HTTP en entreprise avec routage multi-tenant et répartition de charge. Suivez la feuille de route MCP pour les mises à jour qui affectent votre choix de transport.
Conception des fonctionnalités
Le serveur MCP devrait exposer un ensemble minimal d’outils :
search — L’outil principal. Exécute une recherche hybride et renvoie des résultats classés.
{
"name": "obsidian_search",
"description": "Search the Obsidian vault using hybrid BM25 + vector retrieval",
"parameters": {
"query": { "type": "string", "description": "Search query" },
"limit": { "type": "integer", "default": 5 },
"max_tokens": { "type": "integer", "default": 2000 }
}
}
read_note — Lit le contenu complet d’une note spécifique par son chemin. Utile lorsque l’agent souhaite voir le contexte complet d’un résultat de recherche.
{
"name": "obsidian_read_note",
"description": "Read the full content of a note by file path",
"parameters": {
"file_path": { "type": "string", "description": "Relative path within vault" }
}
}
list_notes — Liste les notes correspondant à un filtre (par dossier, tag, type ou plage de dates). Utile pour l’exploration lorsque l’agent n’a pas de requête spécifique.
{
"name": "obsidian_list_notes",
"description": "List notes matching filters",
"parameters": {
"folder": { "type": "string", "description": "Folder path within vault" },
"tag": { "type": "string", "description": "Tag to filter by" },
"limit": { "type": "integer", "default": 20 }
}
}
get_context — Un outil pratique qui exécute une recherche et formate les résultats sous forme de bloc de contexte adapté à l’injection dans une conversation.
{
"name": "obsidian_get_context",
"description": "Get formatted context from vault for a topic",
"parameters": {
"topic": { "type": "string", "description": "Topic to get context for" },
"max_tokens": { "type": "integer", "default": 2000 }
}
}
Limites de permissions
Le serveur MCP doit appliquer des limites strictes :
-
Lecture seule. Le serveur lit le coffre et la base de données d’index. Il ne crée, ne modifie ni ne supprime de notes. Les opérations d’écriture (capture de nouvelles notes) sont gérées par des hooks ou des skills séparés, et non par le serveur MCP.
-
Limité au coffre. Le serveur ne lit que les fichiers situés dans le chemin du coffre configuré. Les tentatives de traversée de chemin (
../../etc/passwd) doivent être rejetées. -
Filtrage des identifiants en sortie. Même si la base de données contient du contenu pré-filtré, appliquez le filtrage des identifiants en sortie comme mesure de défense en profondeur.
-
Réponses limitées en tokens. Appliquez
max_tokenssur toutes les réponses d’outils pour empêcher l’outil IA de recevoir des blocs de contexte excessivement volumineux.
Gestion des erreurs
Les outils MCP devraient renvoyer des messages d’erreur structurés qui aident l’outil IA à récupérer :
def search(self, query, limit=5, max_tokens=2000):
if not self.db_path.exists():
return {
"error": "Index database not found. Run the indexer first.",
"suggestion": "python index_vault.py --full"
}
results = self.retriever.search(query, limit, max_tokens)
if not results:
return {
"results": [],
"message": f"No results found for '{query}'. Try broader terms."
}
return {
"results": [
{
"file_path": r["file_path"],
"section": r["section"],
"text": r["chunk_text"],
"score": round(r["rrf_score"], 4),
}
for r in results
],
"count": len(results),
"query": query,
}
Intégration avec Claude Code
Claude Code est le principal consommateur du système de recherche Obsidian. Cette section couvre la configuration MCP, l’intégration des hooks et le pattern obsidian_bridge.py.
Configuration MCP
Ajoutez le serveur MCP Obsidian à ~/.claude/settings.json :
{
"mcpServers": {
"obsidian": {
"command": "python",
"args": ["/path/to/obsidian_mcp.py"],
"env": {
"VAULT_PATH": "/absolute/path/to/vault",
"DB_PATH": "/absolute/path/to/vectors.db"
}
}
}
}
Après avoir ajouté la configuration, redémarrez Claude Code. Le serveur MCP démarrera en tant que processus enfant. Vérifiez qu’il fonctionne :
> What tools do you have from the obsidian MCP server?
Claude Code devrait lister les outils disponibles (obsidian_search, obsidian_read_note, etc.).
Intégration des hooks
Les hooks étendent le comportement de Claude Code à des points définis du cycle de vie. Deux hooks sont pertinents pour l’intégration avec Obsidian :
Hook PreToolUse — Interroge le coffre avant que l’agent ne traite un appel d’outil. Injecte automatiquement le contexte pertinent.
#!/bin/bash
# ~/.claude/hooks/pre-tool-use/obsidian-context.sh
# Automatically inject vault context before tool execution
TOOL_NAME="$1"
PROMPT="$2"
# Only inject context for code-related tools
case "$TOOL_NAME" in
Edit|Write|Bash)
# Query the vault
CONTEXT=$(python /path/to/retriever.py search "$PROMPT" --limit 3 --max-tokens 1500)
if [ -n "$CONTEXT" ]; then
echo "---"
echo "Relevant vault context:"
echo "$CONTEXT"
echo "---"
fi
;;
esac
Hook PostToolUse — Capture les sorties significatives des outils dans le coffre pour une récupération ultérieure.
#!/bin/bash
# ~/.claude/hooks/post-tool-use/capture-insight.sh
# Capture significant outputs to vault (selective)
TOOL_NAME="$1"
OUTPUT="$2"
# Only capture substantial outputs
if [ ${#OUTPUT} -gt 500 ]; then
python /path/to/capture.py --text "$OUTPUT" --source "claude-code-$TOOL_NAME"
fi
Le pattern obsidian_bridge.py
Un module de pont fournit une Python API que les hooks et les skills peuvent appeler :
# obsidian_bridge.py
from retriever import HybridRetriever
_retriever = None
def get_retriever():
global _retriever
if _retriever is None:
_retriever = HybridRetriever(
db_path="/path/to/vectors.db",
vault_path="/path/to/vault",
)
return _retriever
def search_vault(query, limit=5, max_tokens=2000):
"""Search vault and return formatted context."""
retriever = get_retriever()
results = retriever.search(query, limit, max_tokens)
if not results:
return ""
lines = ["## Vault Context\n"]
for r in results:
lines.append(f"**{r['file_path']}** — {r['section']}")
lines.append(f"> {r['chunk_text'][:500]}")
lines.append("")
return "\n".join(lines)
Le skill /capture
Un skill Claude Code pour capturer des informations dans le coffre :
/capture "OAuth token rotation requires both access and refresh token invalidation"
--domain security
--tags oauth,tokens
Le skill crée une nouvelle note dans 00-inbox/ avec les métadonnées frontmatter appropriées et déclenche une réindexation incrémentale pour que la nouvelle note soit immédiatement consultable.
Gestion de la fenêtre de contexte
L’intégration doit être attentive à la fenêtre de contexte de Claude Code :
- Limitez le contexte injecté à 1 500-2 000 tokens par requête. Au-delà, cela entre en concurrence avec la mémoire de travail de l’agent.
- Incluez l’attribution de la source. Incluez toujours le chemin du fichier et le titre de la section pour que l’agent puisse référencer la source.
- Tronquez le texte des fragments. Les fragments longs devraient être tronqués avec
...plutôt qu’omis entièrement. Les 300 à 500 premiers caractères contiennent généralement les informations clés. - N’injectez pas à chaque appel d’outil. Le hook PreToolUse devrait injecter le contexte de manière sélective en fonction de l’outil appelé. Les opérations de lecture n’ont pas besoin du contexte du coffre. Les opérations d’écriture et de modification en bénéficient.
Intégration de Codex CLI
Codex CLI se connecte aux serveurs MCP via config.toml. Le modèle d’intégration diffère de Claude Code au niveau de la syntaxe de configuration et de la transmission des instructions.
Configuration MCP
Ajoutez dans .codex/config.toml ou ~/.codex/config.toml :
[mcp_servers.obsidian]
command = "python"
args = ["/path/to/obsidian_mcp.py"]
[mcp_servers.obsidian.env]
VAULT_PATH = "/absolute/path/to/vault"
DB_PATH = "/absolute/path/to/vectors.db"
Modèles AGENTS.md
Codex CLI lit AGENTS.md pour les instructions au niveau du projet. Incluez des directives de recherche dans le coffre :
## Available Tools
### Obsidian Vault (MCP: obsidian)
Use the `obsidian_search` tool to find relevant context from the knowledge base.
Search the vault when you need:
- Background on a concept or pattern
- Prior decisions or rationale
- Reference material for implementation
Example queries:
- "authentication patterns in FastAPI"
- "how does the review aggregator work"
- "sqlite-vec configuration"
Différences avec Claude Code
| Fonctionnalité | Claude Code | Codex CLI |
|---|---|---|
| Configuration MCP | settings.json |
config.toml |
| Hooks | ~/.claude/hooks/ |
Non pris en charge |
| Skills | ~/.claude/skills/ |
Non pris en charge |
| Fichier d’instructions | CLAUDE.md |
AGENTS.md |
| Modes d’approbation | --dangerously-skip-permissions |
suggest / auto-edit / full-auto |
Différence clé : Codex CLI ne prend pas en charge les hooks. Le modèle d’injection automatique de contexte (hook PreToolUse) n’est pas disponible. À la place, incluez des instructions explicites dans AGENTS.md indiquant à l’agent de rechercher dans le coffre avant de commencer le travail.
Cursor et autres outils
Cursor et les autres outils d’IA prenant en charge MCP peuvent se connecter au même serveur MCP Obsidian. Cette section couvre la configuration des outils courants.
Cursor
Ajoutez dans .cursor/mcp.json à la racine de votre projet :
{
"mcpServers": {
"obsidian": {
"command": "python",
"args": ["/path/to/obsidian_mcp.py"],
"env": {
"VAULT_PATH": "/absolute/path/to/vault",
"DB_PATH": "/absolute/path/to/vectors.db"
}
}
}
}
Le fichier .cursorrules de Cursor peut inclure des instructions pour utiliser le coffre :
When working on implementation tasks, search the Obsidian vault
for relevant context before writing code. Use the obsidian_search
tool with descriptive queries about the concept you're implementing.
Matrice de compatibilité
| Outil | Support MCP | Transport | Emplacement de configuration |
|---|---|---|---|
| Claude Code | Complet | STDIO | ~/.claude/settings.json |
| Codex CLI | Complet | STDIO | .codex/config.toml |
| Cursor | Complet | STDIO | .cursor/mcp.json |
| Windsurf | Complet | STDIO | .windsurf/mcp.json |
| Continue.dev | Partiel | HTTP | ~/.continue/config.json |
| Zed | En cours | STDIO | Interface des paramètres |
Solution de repli pour les outils sans MCP
Pour les outils qui ne prennent pas en charge MCP, le système de récupération peut être encapsulé en tant que CLI :
# Search from command line
python retriever_cli.py search "query text" --limit 5
# Output formatted for copy-paste into any tool
python retriever_cli.py context "query text" --format markdown
Le CLI produit du texte structuré qui peut être copié-collé manuellement dans l’entrée de n’importe quel outil d’IA. C’est moins élégant qu’une intégration MCP, mais cela fonctionne universellement.
Mise en cache de prompts à partir de notes structurées
Les notes structurées dans le coffre peuvent servir de blocs de contexte réutilisables qui réduisent la consommation de tokens à travers les interactions avec l’IA. Cette section couvre la conception des clés de cache et la gestion du budget de tokens.
Le principe
Au lieu de rechercher du contexte à chaque interaction, pré-construisez des blocs de contexte à partir de notes de coffre bien structurées et mettez-les en cache :
# cache_keys.py
CONTEXT_BLOCKS = {
"auth-patterns": {
"vault_query": "authentication patterns implementation",
"max_tokens": 1500,
"ttl_hours": 24, # Rebuild daily
},
"api-conventions": {
"vault_query": "API design conventions REST patterns",
"max_tokens": 1000,
"ttl_hours": 168, # Rebuild weekly
},
"project-architecture": {
"vault_query": "current project architecture decisions",
"max_tokens": 2000,
"ttl_hours": 12, # Rebuild twice daily
},
}
Invalidation du cache
L’invalidation du cache repose sur deux signaux :
- Expiration du TTL. Chaque bloc de contexte possède une durée de vie (TTL). Lorsque le TTL expire, le bloc est reconstruit en interrogeant à nouveau le coffre.
- Détection des modifications du coffre. Lorsque l’indexeur détecte des modifications dans les fichiers qui ont contribué à un bloc de contexte mis en cache, le bloc est invalidé immédiatement.
Gestion du budget de tokens
Une session démarre avec un budget total de contexte. Les blocs mis en cache consomment une partie de ce budget :
Total context budget: 8,000 tokens
├─ System prompt: 1,500 tokens
├─ Cached blocks: 3,000 tokens (pre-loaded)
├─ Dynamic search: 2,000 tokens (on-demand)
└─ Conversation: 1,500 tokens (remaining)
Les blocs mis en cache sont chargés au démarrage de la session. Les résultats de recherche dynamique comblent le budget restant au cas par cas. Cette approche hybride fournit à l’agent une base de contexte fréquemment nécessaire tout en préservant du budget pour des requêtes spécifiques.
Comparaison de la consommation de tokens
Sans mise en cache : chaque requête pertinente déclenche une recherche dans le coffre, renvoyant 1 500 à 2 000 tokens de contexte. Sur 10 requêtes dans une session, l’agent consomme 15 000 à 20 000 tokens de contexte provenant du coffre.
Avec mise en cache : trois blocs de contexte pré-construits consomment 4 500 tokens au total. Les recherches supplémentaires ajoutent 1 500 à 2 000 tokens par requête unique. Sur 10 requêtes dont 6 sont couvertes par les blocs mis en cache, l’agent consomme 4 500 + (4 × 1 500) = 10 500 tokens — soit environ la moitié de la consommation sans cache.
Hooks PostToolUse pour la compression de contexte
Les sorties des outils peuvent être verbeuses : traces de pile, listes de fichiers, résultats de tests. Un hook PostToolUse peut compresser ces sorties avant qu’elles ne consomment de l’espace dans la fenêtre de contexte.
Le problème
Un appel à l’outil Bash qui exécute des tests peut renvoyer :
PASSED tests/test_auth.py::test_login_success
PASSED tests/test_auth.py::test_login_failure
PASSED tests/test_auth.py::test_token_refresh
PASSED tests/test_auth.py::test_session_expiry
... (200 more lines)
FAILED tests/test_api.py::test_rate_limit_exceeded
La sortie complète fait 5 000 tokens, mais le signal utile tient en 2 lignes : 200 réussis, 1 échoué.
Implémentation du hook
#!/bin/bash
# ~/.claude/hooks/post-tool-use/compress-output.sh
# Compress verbose tool outputs to preserve context window
TOOL_NAME="$1"
OUTPUT="$2"
OUTPUT_LEN=${#OUTPUT}
# Only compress large outputs
if [ "$OUTPUT_LEN" -lt 2000 ]; then
exit 0 # Pass through unchanged
fi
case "$TOOL_NAME" in
Bash)
# Compress test output
if echo "$OUTPUT" | grep -q "PASSED\|FAILED"; then
PASSED=$(echo "$OUTPUT" | grep -c "PASSED")
FAILED=$(echo "$OUTPUT" | grep -c "FAILED")
FAILURES=$(echo "$OUTPUT" | grep "FAILED")
echo "Tests: $PASSED passed, $FAILED failed"
if [ "$FAILED" -gt 0 ]; then
echo "Failures:"
echo "$FAILURES"
fi
fi
;;
esac
Prévention des déclenchements récursifs
Un hook de compression qui émet une sortie pourrait se déclencher lui-même s’il n’est pas protégé :
# Guard against recursive invocation
if [ -n "$COMPRESS_HOOK_ACTIVE" ]; then
exit 0
fi
export COMPRESS_HOOK_ACTIVE=1
Heuristiques de compression
| Type de sortie | Détection | Stratégie de compression |
|---|---|---|
| Résultats de tests | Mots-clés PASSED / FAILED |
Compter les réussites/échecs, afficher uniquement les échecs |
| Listes de fichiers | ls ou find dans la commande |
Tronquer aux 20 premières entrées + total |
| Traces de pile | Mot-clé Traceback |
Conserver le premier et le dernier cadre + message d’erreur |
| Statut Git | modified: / new file: |
Résumer les totaux par statut |
| Sortie de compilation | warning: / error: |
Supprimer les lignes d’information, conserver les avertissements/erreurs |
Pipeline d’intake et de triage des signaux
La couche d’intake détermine ce qui entre dans le coffre-fort. Sans curation, le coffre-fort accumule du bruit. Cette section couvre le pipeline de scoring qui achemine les signaux vers les dossiers de domaine.
Sources
Les signaux proviennent de multiples canaux :
- Flux RSS : Blogs techniques, avis de sécurité, notes de version
- Favoris : Favoris du navigateur sauvegardés via Obsidian Web Clipper ou bookmarklet
- Newsletters : Extraits clés de newsletters par e-mail
- Capture manuelle : Notes rédigées pendant la lecture, les conversations ou la recherche
- Sorties d’outils : Sorties significatives d’outils IA capturées via des hooks
Dimensions de scoring
Chaque signal est évalué sur quatre dimensions (de 0.0 à 1.0 chacune) :
| Dimension | Question | Score faible (0.0-0.3) | Score élevé (0.7-1.0) |
|---|---|---|---|
| Pertinence | Est-ce lié à mes domaines actifs ? | Tangentiel, hors périmètre | Directement pertinent pour le travail en cours |
| Actionnabilité | Puis-je utiliser cette information ? | Théorie pure, aucune application | Technique ou pattern spécifique que je peux appliquer |
| Profondeur | Quel est le niveau de substance du contenu ? | Titres, résumé superficiel | Analyse détaillée avec exemples |
| Autorité | Quelle est la crédibilité de la source ? | Blog anonyme, non vérifié | Source primaire, évaluée par des pairs, expert reconnu |
Score composite et routage
composite = (relevance * 0.35) + (actionability * 0.25) +
(depth * 0.25) + (authority * 0.15)
| Plage de score | Action |
|---|---|
| 0.55+ | Routage automatique vers le dossier de domaine |
| 0.40 - 0.55 | Mise en file d’attente pour revue manuelle |
| < 0.40 | Suppression (ne pas stocker) |
Routage par domaine
Les signaux avec un score supérieur à 0.55 sont acheminés vers l’un des 12 dossiers de domaine en fonction de la correspondance de mots-clés et de la classification thématique :
05-signals/
├── ai-tooling/ # Claude, LLMs, AI development tools
├── security/ # Vulnerabilities, auth, cryptography
├── systems/ # Architecture, distributed systems
├── programming/ # Languages, patterns, algorithms
├── web/ # Frontend, backends, APIs
├── data/ # Databases, data engineering
├── devops/ # CI/CD, containers, infrastructure
├── design/ # UI/UX, product design
├── mobile/ # iOS, Android, cross-platform
├── career/ # Industry trends, hiring, growth
├── research/ # Academic papers, whitepapers
└── other/ # Signals that don't fit a domain
Statistiques de production
Sur 14 mois d’exploitation :
| Métrique | Valeur |
|---|---|
| Total de signaux traités | 7 771 |
| Routage automatique (>0.55) | 4 832 (62 %) |
| En file d’attente pour revue (0.40-0.55) | 1 543 (20 %) |
| Supprimés (<0.40) | 1 396 (18 %) |
| Dossiers de domaine actifs | 12 |
| Moyenne de signaux par jour | ~18 |
Patterns de graphe de connaissances
Le graphe de wiki-link d’Obsidian encode les relations entre les notes. Cette section couvre la sémantique des liens, la traversée de graphe pour l’expansion de contexte, et les anti-patterns qui dégradent la qualité du graphe.
Sémantique des backlinks
Chaque wiki-link crée une arête dirigée dans le graphe. Obsidian suit à la fois les liens directs et les backlinks :
- Lien direct : La note A contient
[[Note B]]→ A pointe vers B - Backlink : La note B indique que la note A la référence
Le graphe encode différents types de relations selon le contexte :
| Pattern de lien | Sémantique | Exemple |
|---|---|---|
| Lien inline | « Est lié à » | “See [[OAuth Token Rotation]] for details” |
| Lien d’en-tête | « A pour sous-thème » | ”## Related\n- [[Token Rotation]]\n- [[Session Management]]” |
| Lien de type tag | « Est catégorisé comme » | ”[[type/reference]]” |
| Lien MOC | « Fait partie de » | Une note Maps of Content listant les notes associées |
Maps of Content (MOCs)
Les MOCs sont des notes d’index qui organisent les notes associées en une structure navigable :
---
title: "Authentication & Security MOC"
type: moc
domain: security
---
## Core Concepts
- [[OAuth 2.0 Overview]]
- [[JWT Token Anatomy]]
- [[Session Management Patterns]]
## Implementation Patterns
- [[OAuth Token Rotation]]
- [[Refresh Token Security]]
- [[PKCE Flow Implementation]]
## Failure Modes
- [[Token Expiry Handling]]
- [[Session Fixation Prevention]]
- [[CSRF Defense Strategies]]
Les MOCs bénéficient à la recherche de deux façons :
- Correspondance directe. Une recherche pour « authentication overview » correspond au MOC lui-même, fournissant à l’agent une liste organisée de notes associées.
- Expansion de contexte. Après avoir trouvé une note spécifique, le système de recherche peut vérifier si la note apparaît dans des MOCs et inclure la structure du MOC dans les résultats, offrant à l’agent une carte du sujet plus large.
Traversée de graphe pour l’expansion de contexte
Une amélioration future du système de recherche : après avoir trouvé les meilleurs résultats, étendre le contexte en suivant les liens :
def expand_context(results, depth=1):
"""Follow wiki-links from top results to find related context."""
expanded = set()
for result in results:
# Parse wiki-links from chunk text
links = extract_wiki_links(result["chunk_text"])
for link_target in links:
# Resolve link to file path
target_path = resolve_wiki_link(link_target)
if target_path and target_path not in expanded:
expanded.add(target_path)
# Include target's most relevant chunk
target_chunks = get_chunks_for_file(target_path)
# ... rank and include best chunk
return results + list(expanded_results)
Ceci n’est pas implémenté dans le système de recherche actuel, mais représente une extension naturelle de la structure de graphe.
Anti-patterns
Clusters orphelins. Groupes de notes qui se lient entre elles mais n’ont aucune connexion avec le reste du coffre-fort. Le panneau de graphe dans Obsidian les rend visibles sous forme d’îlots déconnectés. Les clusters orphelins indiquent des MOCs manquants ou des liens inter-domaines absents.
Prolifération de tags. Utiliser les tags de manière incohérente ou créer trop de tags trop granulaires. Un coffre-fort avec 500 tags uniques sur 5 000 notes donne en moyenne 1 note pour 10 tags — les tags ne sont pas utiles pour le filtrage. Consolidez vers 20 à 50 tags de haut niveau qui correspondent à vos dossiers de domaine.
Notes riches en liens, pauvres en contenu. Notes constituées uniquement de wiki-links sans prose. Ces notes s’indexent mal car le système de découpage n’a pas de texte à transformer en embeddings. Ajoutez au moins un paragraphe de contexte expliquant pourquoi les notes liées sont en rapport.
Liens bidirectionnels pour tout. Chaque référence n’a pas besoin d’être un wiki-link. Mentionner « OAuth » en passant ne nécessite pas [[OAuth 2.0 Overview]]. Réservez les wiki-links pour des relations intentionnelles et navigables où cliquer sur le lien fournirait un contexte utile.
Recettes de workflow pour développeurs
Workflows pratiques qui combinent la recherche dans le coffre-fort avec les tâches de développement quotidiennes.
Chargement de contexte matinal
Commencez la journée en chargeant le contexte pertinent :
Search my vault for notes about [current project] updated in the last week
Le système de recherche renvoie les notes récentes concernant votre projet actif, vous offrant un rafraîchissement rapide de là où vous en étiez. Plus efficace que de relire les messages de commit de la veille.
Capture de recherche pendant le développement
Pendant l’implémentation d’une fonctionnalité, capturez des informations sans quitter l’éditeur :
/capture "FastAPI dependency injection with async generators requires yield,
not return. The generator is the dependency lifecycle."
--domain programming
--tags fastapi,dependency-injection
L’information capturée est immédiatement indexée et disponible pour une recherche future. Au fil des mois, ces micro-captures construisent un corpus de connaissances spécifiques à l’implémentation.
Lancement de projet
Lors du démarrage d’un nouveau projet ou d’une nouvelle fonctionnalité :
- Rechercher dans le coffre-fort : « Qu’est-ce que je sais sur [technologie/pattern] ? »
- Examiner les 5 meilleurs résultats pour les décisions antérieures et les pièges
- Vérifier si un MOC existe pour le domaine ; sinon, en créer un
- Rechercher les modes de défaillance : « problèmes avec [technologie] »
Débogage avec la recherche dans le coffre-fort
Face à une erreur ou un comportement inattendu :
Search my vault for [error message or symptom]
Les notes de débogage antérieures contiennent souvent la cause racine et le correctif. Ceci est particulièrement précieux pour les problèmes récurrents entre projets — le coffre-fort se souvient de ce que vous oubliez.
Préparation de revue de code
Avant de réviser une PR :
Search my vault for patterns and conventions about [module being changed]
Le coffre-fort renvoie les décisions antérieures, les contraintes architecturales et les standards de code pertinents pour le code en cours de revue. La revue est informée par la connaissance institutionnelle, pas seulement par le diff.
Optimisation des performances
Cette section couvre les stratégies d’optimisation pour différentes tailles de coffre-fort et différents modes d’utilisation.
Gestion de la taille de l’index
| Taille du coffre-fort | Fragments | Taille BDD | Réindexation complète | Incrémentale |
|---|---|---|---|---|
| 500 notes | ~1 500 | 3 Mo | 15 secondes | <1 seconde |
| 2 000 notes | ~6 000 | 12 Mo | 45 secondes | 2 secondes |
| 5 000 notes | ~15 000 | 30 Mo | 2 minutes | 4 secondes |
| 15 000 notes | ~50 000 | 83 Mo | 4 minutes | <10 secondes |
| 50 000 notes | ~150 000 | 250 Mo | 15 minutes | 30 secondes |
Au-delà de 50 000 notes, envisagez : - D’augmenter la taille des lots de 64 à 128 pour accélérer la génération d’embeddings - D’utiliser le mode WAL (par défaut) pour l’accès concurrent - D’exécuter la réindexation complète en dehors des heures d’utilisation
Optimisation des requêtes
Mode WAL. Le mode Write-Ahead Logging de SQLite permet des lectures concurrentes pendant que l’indexeur écrit :
db.execute("PRAGMA journal_mode=WAL")
C’est essentiel lorsque le serveur MCP traite des requêtes pendant que l’indexeur exécute une mise à jour incrémentale.
Réutilisation des connexions. Le serveur MCP devrait réutiliser les connexions à la base de données plutôt que d’ouvrir une nouvelle connexion par requête. Une connexion unique et persistante avec le mode WAL prend en charge les lectures concurrentes.
# MCP server initialization
db = sqlite3.connect(DB_PATH, check_same_thread=False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA mmap_size=268435456") # 256 MB mmap
E/S mappées en mémoire. Le pragma mmap_size indique à SQLite d’utiliser des E/S mappées en mémoire pour le fichier de base de données. Pour une base de 83 Mo, mapper l’intégralité du fichier en mémoire élimine la plupart des lectures disque.
Optimisation FTS5. Après une réindexation complète, exécutez :
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');
Cela fusionne les segments internes de l’arbre B de FTS5, réduisant la latence des requêtes pour les recherches ultérieures.
Benchmarks de mise à l’échelle
Mesuré sur Apple M3 Pro, 36 Go de RAM, SSD NVMe :
| Opération | 500 notes | 5K notes | 15K notes | 50K notes |
|---|---|---|---|---|
| Requête BM25 | 2ms | 5ms | 12ms | 25ms |
| Requête vectorielle | 1ms | 3ms | 8ms | 20ms |
| Fusion RRF | <1ms | <1ms | 3ms | 5ms |
| Recherche complète | 3ms | 8ms | 23ms | 50ms |
Tous les benchmarks incluent l’accès à la base de données, l’exécution des requêtes et le formatage des résultats. La latence réseau pour la communication MCP STDIO ajoute 1 à 2 ms.
Dépannage
Désynchronisation de l’index
Symptôme : La recherche renvoie des résultats obsolètes ou ne trouve pas les notes récemment ajoutées.
Cause : L’indexeur incrémental ne s’est pas exécuté après l’ajout de notes, ou le mtime d’un fichier n’a pas été mis à jour (par exemple, synchronisé depuis une autre machine avec les horodatages préservés).
Solution : Lancez une réindexation complète : python index_vault.py --full
Changement de modèle d’embeddings
Symptôme : Après avoir changé le modèle d’embeddings, la recherche vectorielle renvoie des résultats incohérents.
Cause : Les anciens vecteurs (issus du modèle précédent) sont comparés aux nouveaux vecteurs de requête. Les dimensions ou la sémantique de l’espace vectoriel sont incompatibles.
Solution : L’indexeur devrait détecter la différence de hash du modèle et déclencher automatiquement une réindexation complète. Si ce n’est pas le cas, supprimez manuellement la base de données et réindexez :
rm vectors.db
python index_vault.py --full
Maintenance FTS5
Symptôme : Les requêtes FTS5 renvoient des résultats incorrects ou incomplets après de nombreuses mises à jour incrémentales.
Cause : Les segments internes de FTS5 peuvent se fragmenter après de nombreuses petites mises à jour.
Solution : Reconstruisez et optimisez :
INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild');
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');
Délai d’expiration MCP
Symptôme : L’outil d’IA signale que le serveur MCP a expiré.
Cause : La première requête déclenche le chargement du modèle (initialisation paresseuse), ce qui prend 2 à 5 secondes. Le délai d’expiration MCP par défaut de l’outil d’IA peut être plus court.
Solution : Préchauffez le modèle au démarrage du serveur :
# In MCP server initialization
retriever = HybridRetriever(db_path, vault_path)
retriever.search("warmup", limit=1) # Trigger model load
Verrouillage de fichiers SQLite
Symptôme : Erreurs SQLITE_BUSY ou SQLITE_LOCKED.
Cause : Plusieurs processus écrivent simultanément dans la base de données. Le mode WAL autorise les lectures concurrentes mais un seul processus en écriture.
Solution : Assurez-vous qu’un seul processus (l’indexeur) écrit dans la base de données. Le serveur MCP et les hooks ne doivent effectuer que des lectures. Si vous avez besoin d’écritures concurrentes, utilisez le mode WAL et définissez un délai d’attente :
db.execute("PRAGMA busy_timeout=5000") # Wait up to 5 seconds
sqlite-vec ne se charge pas
Symptôme : La recherche vectorielle est désactivée ; le système de recherche fonctionne en mode BM25 uniquement.
Cause : L’extension sqlite-vec n’est pas installée, n’est pas trouvée dans le chemin de la bibliothèque, ou est incompatible avec la version de SQLite.
Solution :
# Install via pip
pip install sqlite-vec
# Or compile from source
git clone https://github.com/asg017/sqlite-vec
cd sqlite-vec && make
Vérifiez que l’extension se charge correctement :
import sqlite3
db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
db.load_extension("vec0")
print("sqlite-vec loaded successfully")
Problèmes de mémoire avec les grands coffres-forts
Symptôme : Erreurs de mémoire insuffisante lors de la réindexation complète d’un grand coffre-fort (50 000+ notes).
Cause : La taille des lots d’embeddings est trop importante, ou le contenu de tous les fichiers est chargé en mémoire simultanément.
Solution : Réduisez la taille des lots et traitez les fichiers de manière incrémentale :
BATCH_SIZE = 32 # Reduce from 64
Assurez-vous également que l’indexeur traite les fichiers un par un (lecture, découpage en fragments et génération d’embeddings pour chaque fichier avant de passer au suivant) plutôt que de charger tous les fichiers en mémoire.
Guide de migration
Depuis Apple Notes
- Exportez Apple Notes via l’option « Tout exporter » (macOS) ou utilisez un outil de migration comme
apple-notes-liberator - Convertissez les exports HTML en markdown avec
markdownifyoupandoc - Déplacez les fichiers convertis dans le dossier
00-inbox/de votre coffre-fort - Vérifiez et ajoutez le frontmatter à chaque note
- Déplacez les notes dans les dossiers de domaine appropriés
Depuis Notion
- Exportez depuis Notion : Paramètres → Exporter → Markdown & CSV
- Décompressez l’export dans le dossier
00-inbox/de votre coffre-fort - Corrigez les artefacts markdown spécifiques à Notion :
- Notion utilise
- [ ]pour les listes de tâches — c’est du markdown standard - Notion inclut des tables de propriétés en HTML — convertissez-les en frontmatter YAML
- Notion intègre les images avec des chemins relatifs — copiez les images dans votre dossier de pièces jointes
- Ajoutez le frontmatter standard (
type,domain,tags) - Remplacez les liens de pages Notion par des wiki-links Obsidian
Depuis Google Docs
- Utilisez Google Takeout pour exporter tous les documents
- Convertissez les fichiers
.docxen markdown :pandoc -f docx -t markdown input.docx -o output.md - Conversion par lot :
for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done - Déplacez les fichiers dans le coffre-fort, ajoutez le frontmatter, organisez dans des dossiers
Depuis du Markdown brut (sans Obsidian)
Si vous disposez déjà d’un répertoire de fichiers markdown :
- Ouvrez le répertoire comme coffre-fort Obsidian (Obsidian → Ouvrir un coffre-fort → Ouvrir un dossier)
- Ajoutez
.obsidian/au.gitignoresi le répertoire est versionné - Créez des modèles de frontmatter et appliquez-les aux fichiers existants
- Commencez à lier les notes avec des
[[wiki-links]]au fur et à mesure de votre lecture et de votre organisation - Lancez l’indexeur immédiatement — le système de recherche fonctionne dès le premier jour
Depuis un autre système de recherche
Si vous migrez depuis un système d’embeddings/recherche différent :
- N’essayez pas de migrer les vecteurs. Différents modèles produisent des espaces vectoriels incompatibles. Lancez une réindexation complète avec le nouveau modèle.
- Migrez le contenu, pas l’index. Les fichiers du coffre-fort sont la source de vérité. L’index est un artefact dérivé.
- Vérifiez après la migration. Exécutez 10 à 20 requêtes dont vous connaissez les réponses et vérifiez que les résultats correspondent à vos attentes.
Journal des modifications
| Date | Modification |
|---|---|
| 2026-03-03 | Mise à jour de l’évolution de la spécification MCP (novembre 2025 : Streamable HTTP, .well-known, annotations d’outils). Ajout du fine-tuning Model2Vec et du support du tokenizer BPE/Unigram. Ajout du tableau comparatif des serveurs MCP communautaires. Mise à jour de Smart Connections vers la v4. |
| 2026-03-02 | Ajout de potion-base-32M et potion-retrieval-32M à la comparaison des modèles. Ajout de la section quantification/réduction de dimensionnalité. Ajout de la note sur l’évolution de la spécification MCP. |
| 2026-03-01 | Publication initiale |
Références
-
Cormack, G.V., Clarke, C.L.A., and Buettcher, S. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR, 2009. Introduit RRF avec k=60 comme méthode sans paramètre pour combiner des listes classées. ↩↩↩
-
OpenAI Embeddings Pricing. text-embedding-3-small : 0,02 $ par million de tokens. Coût estimé du coffre-fort par réindexation complète : ~0,30 $. ↩
-
van Dongen, T. et al. Model2Vec: Turn any Sentence Transformer into a Small Fast Model. arXiv, 2025. Décrit l’approche de distillation produisant des embeddings statiques à partir de transformeurs de phrases. ↩
-
MTEB: Massive Text Embedding Benchmark. potion-base-8M obtient un score moyen de 50,03 contre 56,09 pour all-MiniLM-L6-v2 (rétention de 89 %). ↩
-
SQLite FTS5 Extension. FTS5 fournit la recherche en texte intégral avec classement BM25 et pondération configurable des colonnes. ↩
-
sqlite-vec: A vector search SQLite extension. Fournit des tables virtuelles
vec0pour la recherche vectorielle KNN au sein de SQLite. ↩ -
Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. ↩
-
Karpukhin, V. et al. Dense Passage Retrieval for Open-Domain Question Answering. EMNLP, 2020. Les représentations denses surpassent BM25 de 9 à 19 % sur les questions-réponses en domaine ouvert. ↩
-
Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. Travaux fondateurs sur la similarité sémantique dense. ↩
-
Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. La recherche hybride (hybrid retrieval) surpasse systématiquement les approches à modalité unique sur MS MARCO. ↩
-
SQLite Write-Ahead Logging. Mode WAL pour les lectures concurrentes avec un seul processus d’écriture. ↩
-
Gao, Y. et al. Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv, 2024. Panorama des architectures RAG et des stratégies de découpage (chunking). ↩
-
Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. ↩
-
Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. ↩
-
Obsidian Documentation. Documentation officielle d’Obsidian. ↩
-
Model Context Protocol Specification. Le standard MCP pour connecter les outils d’IA aux sources de données. ↩
-
Données de production de l’auteur. 16 894 fichiers, 49 746 fragments, base de données SQLite de 83,56 Mo, 7 771 signaux traités sur 14 mois. Latence des requêtes mesurée via
time.perf_counter(). ↩ -
Model2Vec Potion Models. Minish Lab, 2025. Potion-base-32M (MTEB 52,46), potion-retrieval-32M (MTEB retrieval 36,35), et fonctionnalités de quantification/réduction de dimensionnalité à partir de la v0.5.0+. ↩↩↩
-
Update on the Next MCP Protocol Release. La version de novembre 2025 a introduit le transport Streamable HTTP, la découverte par URL .well-known, les annotations structurées d’outils et la standardisation des niveaux SDK. La prochaine version, provisoirement prévue pour mi-2026, inclura les opérations asynchrones, les extensions spécifiques aux domaines et la communication entre agents. ↩↩
-
Model2Vec Releases. v0.4.0 (fév. 2025) : prise en charge de l’entraînement et du fine-tuning. v0.5.0 (avr. 2025) : réécriture du backend, quantification, réduction de dimensionnalité. v0.7.0 (oct. 2025) : quantification du vocabulaire, prise en charge des tokeniseurs BPE/Unigram. ↩↩
-
Smart Connections for Obsidian. Smart Connections v4 : embeddings IA en local d’abord, la recherche sémantique fonctionne hors ligne après l’indexation initiale. ↩