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 : 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 récupération 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 récupération du contenu exploitable. Même 10 à 20 notes suffisent pour voir 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
Le serveur communautaire obsidian-mcp offre un accès immédiat au coffre-fort. Installez-le :
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 des 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. C’est la version minimale viable.
Ce que ce démarrage rapide ne vous offre PAS : - La récupération hybride (BM25 + recherche vectorielle + fusion RRF) - La recherche sémantique par embeddings - Le filtrage des informations sensibles - L’indexation incrémentale - L’injection automatique de contexte via les hooks
Le reste de ce guide couvre la construction de chacune de ces fonctionnalités. Le démarrage rapide prouve le concept. Le pipeline complet offre une récupération de qualité production.
Cadre de décision : Obsidian face aux alternatives
Obsidian ne convient pas à tous les cas d’utilisation. Cette section définit quand Obsidian est le bon substrat, quand il est surdimensionné et quand autre chose 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 | Plus de 1 800 plugins | Intégrations | Aucun | N/A | N/A |
| Utilisable hors ligne | Complet | Lecture seule en cache | Partiel | Complet | Complet |
| Passe à l’échelle 10 000+ notes | Oui | Oui (avec API) | Se dégrade | Oui | Non (fichier unique) |
| Coût | Gratuit (cœur) | 10 $/mois+ | Gratuit | Gratuit | Gratuit |
Quand Obsidian est surdimensionné
- Contexte mono-projet. Si l’IA n’a besoin que du contexte du code en cours, placez-le dans
CLAUDE.md,AGENTS.mdou la documentation au niveau du projet. Ces fichiers accompagnent le dépôt et sont automatiquement chargés. - Données structurées. Si le contenu est constitué de tables, d’enregistrements ou de schémas, utilisez une base de données. Les notes Obsidian privilégient la prose. Dataview peut interroger les champs 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 récupération pour du contenu éphémère.
Quand Obsidian est le bon choix
- Accumulation de connaissances sur des mois ou des années. La valeur s’accroît avec le corpus. 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.
- Domaines multiples dans un seul 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 récupération inter-domaines qu’un
CLAUDE.mdspécifique à un projet ne peut pas offrir. - Contenu sensible en matière de confidentialité. Le principe local d’abord signifie que le pipeline de récupération n’envoie jamais de contenu à des services externes. Le coffre-fort contient tout ce que vous y mettez, y compris du contenu que vous ne téléchargeriez pas vers un service cloud.
Modèle mental : trois couches
Le système comporte trois couches qui fonctionnent indépendamment mais dont l’effet se multiplie 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’intake 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, pensées inachevées sans contexte. La couche d’intake est responsable du contrôle qualité au point d’entrée. Un pipeline de notation, une convention de balisage ou un processus de revue manuelle — tout mécanisme qui garantit que le coffre-fort contient du contenu qui mérite d’être récupéré.
La récupération rend le coffre-fort interrogeable. C’est le moteur : découpage des notes en unités de recherche, projection des segments dans un espace vectoriel (embeddings), indexation pour la recherche par mots-clés et sémantique, fusion des résultats avec RRF. La couche de récupération 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 pas accessible programmatiquement aux outils d’IA.
L’intégration connecte la couche de récupération aux outils d’IA. Un serveur MCP expose la récupération comme un outil appelable. Les hooks injectent du contexte automatiquement. Les skills capturent de nouvelles connaissances et les réintègrent 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 notation de l’intake ne connaît rien des embeddings. Le système de récupération ne connaît rien des règles de routage des signaux. Le serveur MCP ne sait pas comment les notes ont été créées. Ce découplage signifie que vous pouvez améliorer chaque couche indépendamment. Remplacez le modèle d’embeddings sans modifier le pipeline d’intake. Ajoutez une nouvelle fonctionnalité MCP sans modifier le système de récupération. Changez les heuristiques de notation 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 modèles 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 une priorité — ils regroupent des domaines connexes et rendent la structure facile à parcourir.
vault/
├── 00-inbox/ # Captures non triées, en attente de classement
├── 01-projects/ # Notes de projets actifs
├── 02-areas/ # Domaines de responsabilité continus
├── 03-resources/ # Matériel de référence par sujet
│ ├── programming/
│ ├── security/
│ ├── ai-engineering/
│ ├── design/
│ └── devops/
├── 04-archive/ # Projets terminés, anciennes références
├── 05-signals/ # Signaux entrants notés
│ ├── ai-tooling/
│ ├── security/
│ ├── systems/
│ └── ...12 domain folders
├── 06-daily/ # Notes quotidiennes (si utilisées)
├── 07-templates/ # Modèles de notes (exclus de l'index)
├── 08-attachments/ # Images, PDF (exclus de l'index)
├── .obsidian/ # Configuration Obsidian (exclue de l'index)
└── .indexignore # Chemins à exclure de l'index de recherche
Dossiers qui doivent être indexés : tout ce qui contient de la prose en markdown — projets, domaines, ressources, signaux, notes quotidiennes.
Dossiers qui doivent être exclus de l’indexation : les modèles (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 correspond à 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 situés dans les chemins exclus ne sont jamais découpés en fragments, jamais transformés en embeddings, et n’apparaissent jamais dans les résultats de recherche.
Schéma des notes
Chaque note doit comporter un YAML frontmatter. Le système de recherche utilise les champs 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 titres pour BM25type— Permet les requêtes filtrées par type (« afficher uniquement les MOCs » ou « uniquement les signaux »)tags— Indexés dans le contexte des titres 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 les requêtes limitées à un domaine (« rechercher uniquement dans les notes de sécurité »)source— Attribution pour le contenu capturé ; le système de recherche peut inclure les URL sources dans les résultatsstatus— Permet d’exclure les notes archivées ou en brouillon de la recherche active
Conventions de découpage
Le système de recherche découpe aux limites des titres 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 fragments recherchables indépendamment. Chaque fragment contient suffisamment de contexte pour que l’embedding capture sa signification. Une requête sur « la gestion des tokens expirés » correspond spécifiquement au troisième fragment.
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 titres H2 produit un seul grand fragment. L’embedding fait la moyenne de tous les sujets de la section. Une requête sur n’importe quel sous-sujet correspond à la note entière de manière égale.
Règle générale : si une section couvre plus d’un concept, divisez-la en sous-sections H2. Le système de découpage s’occupe 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 système 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 système de recherche indexe le texte markdown. Une image sans texte alternatif ou 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), examinez-la et annotez-la avant qu’elle n’entre dans le coffre-fort permanent. Les imports automatiques non trié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 coffre-fort pour la recherche par IA se répartissent en trois catégories : structurels (imposent la cohérence), d’interrogation (exposent les métadonnées) et de synchronisation (maintiennent le coffre-fort à jour).
Plugins essentiels
Dataview. Interroge votre coffre-fort comme une base de données en utilisant les champs frontmatter. Créez des index dynamiques : « toutes les notes taguées 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 recherche, mais vous aide à identifier les lacunes dans la couverture de votre coffre-fort et à trouver les notes qui nécessitent 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 de la recherche.
<%* /* 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
## Références
**Linter.** Applique des règles de formatage dans l'ensemble du coffre-fort. Une hiérarchie de titres cohérente (H1 pour le titre, H2 pour les sections, H3 pour les sous-sections) garantit que le découpeur produit des résultats prévisibles. Règles du Linter importantes pour la recherche :
- Incrémentation des titres : appliquer des niveaux de titres séquentiels (pas de saut de H1 à H3)
- Titre YAML : correspondre au nom du fichier
- Espaces en fin de ligne : supprimer (évite les artefacts de tokenisation FTS5)
- Lignes vides consécutives : limiter à 1 (découpage plus propre)
**Intégration Git.** Contrôle de version pour votre coffre-fort. Suivez les modifications au fil du temps, synchronisez entre les machines et récupérez les suppressions accidentelles. Git fournit également les données `mtime` que l'indexeur utilise pour la détection incrémentielle des changements.
### Plugins qui améliorent l'indexation
**Smart Connections.** Un plugin Obsidian qui fournit une recherche sémantique alimentée par l'IA directement dans Obsidian. Il crée son propre index d'embeddings. Bien que le système de recherche décrit dans ce guide soit externe à Obsidian (il s'exécute en tant que pipeline Python), Smart Connections est utile pour explorer les relations sémantiques pendant l'écriture. 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 retriever externe pour l'intégration avec les outils d'IA.
**Metadata Menu.** Fournit 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 recherche.
### Plugins qui nuisent à l'indexation
**Excalidraw.** Stocke les dessins sous forme de JSON intégré dans les fichiers markdown. Le JSON est syntaxiquement valide en markdown mais produit des résultats incohérents lorsqu'il est découpé 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 recherche de prose. Le découpeur 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 un contenu substantiel ou excluez le dossier de notes quotidiennes de l'index.
### Configuration des plugins qui compte
**Récupération de fichiers → Activée.** Protège contre la suppression accidentelle de notes. Pas directement lié à la recherche, mais critique pour une base de connaissances dont vous dépendez.
**Sauts de ligne stricts → 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>`).
**Emplacement par défaut des nouveaux fichiers → Dossier désigné.** Dirigez les nouveaux fichiers vers `00-inbox/` pour 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.
**Format wiki-link → Chemin le plus court possible.** Des cibles de liens plus courtes sont plus faciles à résoudre pour le retriever lors de l'indexation de la structure des liens.
---
## Modèles d'embeddings : choix et configuration
Le modèle d'embeddings convertit les chunks 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 d'exécution. Cette section explique pourquoi potion-base-8M de Model2Vec est le choix par défaut et quand choisir 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, pas de PyTorch)
**Inférence :** CPU uniquement, embeddings de mots statiques (pas de couches d'attention)
Model2Vec distille les connaissances d'un sentence transformer en embeddings de tokens statiques. 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 plus rapide que les modèles basés sur des transformers, car il n'y a pas de 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 pour les avantages de vitesse et de simplicité. Pour des chunks markdown courts (en moyenne 200 à 400 mots dans un coffre-fort typique), la différence de qualité est moins prononcée que sur des documents plus longs, car les deux modèles convergent vers des représentations similaires pour des textes courts et ciblés.
### Configuration
```python
# 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 paresseux. Le modèle se charge à la première utilisation, pas au moment de l’import. Importer le module embedder ne coûte rien lorsque le retriever fonctionne en mode de repli BM25 uniquement (par exemple, lorsque le venv d’embeddings n’est pas installé).
Environnement virtuel isolé. Le modèle s’exécute dans un venv dédié (par exemple, ~/.claude/venvs/memory/) pour éviter les conflits de dépendances avec le reste de la chaîne d’outils. La fonction _activate_venv() ajoute le 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 le surcoût de Model2Vec. L’indexeur alimente embed_batch() avec des chunks plutôt que d’encoder un chunk à la fois.
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, pas de GPU |
| all-MiniLM-L6-v2 | 384 | 80 Mo | 1x | 56,09 | Qualité supérieure, 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 all-MiniLM-L6-v2 lorsque la qualité de recherche compte plus que la vitesse et que vous avez PyTorch installé. Les vecteurs de 384 dimensions augmentent la taille de la base de données SQLite d’environ 50 % par rapport aux vecteurs de 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 M-series.
Choisissez nomic-embed-text-v1.5 lorsque vous avez besoin de la meilleure qualité de recherche locale possible et que vous acceptez une indexation plus lente. Les vecteurs de 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. Le API produit les embeddings de la plus haute qualité mais introduit une dépendance cloud, un coût par token (0,02 $/million de tokens) et envoie votre contenu aux serveurs d’OpenAI.
Restez avec 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 de 256 dimensions maintiennent la base de données compacte.
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’embeddings, l’indexeur détecte l’incompatibilité lors de la prochaine exécution incrémentielle 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 le mélange de 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 retriever se rabat sur le 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 avec des dimensions incompatibles.
Pression mémoire sur les grands coffres-forts. Encoder plus de 50 000 chunks 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 reste un problème, réduisez la taille des lots.
Recherche plein texte avec FTS5
L’extension FTS5 de SQLite fournit une recherche plein texte avec un 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é les besoins en stockage, mais implique que FTS5 doit être synchronisé manuellement lorsque des chunks sont insérés, mis à jour ou supprimés.
Colonnes. Trois colonnes sont indexées :
- chunk_text — Le contenu principal de chaque chunk (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() dans 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 - Drapeaux CLI :
--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 correspond pas 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 passe à côté car « state management » est exprimé à travers des noms de technologies spécifiques.
BM25 échoue également face à la collision de mots-clés à grande échelle. Dans un coffre-fort de 15 000 fichiers, une recherche pour « configuration » correspond à des centaines de notes car pratiquement 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 « configuration » est pertinente pour la requête en cours.
Tokeniseur FTS5
FTS5 utilise le tokeniseur unicode61 par défaut, qui gère le texte ASCII et Unicode. Pour les coffres-forts contenant un volume important de contenu CJK (chinois, japonais, coréen), envisagez le tokeniseur 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 tokeniseur par défaut unicode61 découpe le texte aux limites de mots, ce qui fonctionne mal pour les langues sans espaces entre les mots. Le tokeniseur trigram découpe tous les trois caractères, permettant la recherche par sous-chaîne 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 intègre 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 modèles 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 chunks.
Pipeline d’embeddings
Le pipeline va de la note au 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 chunks 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 de plus proches voisins approximative. 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 compte 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 peine avec les identifiants exacts :
- Requête :
_rrf_fuse→ Renvoie des notes sur « fusion algorithms » et « rank merging » mais peut classer la définition réelle de la fonction plus bas que des discussions conceptuelles - Requête :
PostToolUse→ Renvoie des notes sur « tool lifecycle hooks » et « post-execution handlers » plutôt que le nom spécifique du hook
La recherche vectorielle peine également 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 modèles structurels plutôt que du sens sémantique. Un fichier JSON avec "review": true produit 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 récupérateur bascule vers 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 récupérateur vérifie vec_available avant de tenter des requêtes vectorielles. Lorsqu’il est désactivé, toutes les recherches utilisent BM25 uniquement, 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, une trace de requête détaillée, 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 hybrid retriever.
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, d’après 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 (1.0 par défaut)
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 cosine. Les scores BM25 sont non bornés et varient avec la taille du corpus. Les distances cosine sont bornées [0, 2]. Les combiner requiert une normalisation, et les paramètres de normalisation dépendent du jeu de données. RRF n’utilise que les positions de rang, qui sont toujours des entiers commençant à 1, quelle que soit la méthode de notation.
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. Vous devriez juger 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 fragment en position 1 (correspondance sémantique sur la résolution de conflits). Après la fusion RRF :
| Fragment | 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 fragments bien classés dans les deux listes remontent en tête. Les fragments qui n’apparaissent que dans une seule liste obtiennent un score à source unique et se retrouvent en dessous des résultats doublement classés. La logique de résolution de désaccords l’emporte car les deux méthodes l’ont trouvée — BM25 grâce aux mots-clés, la recherche vectorielle grâce à la sémantique.
Pour la trace complète étape par étape avec le calcul RRF par rang, essayez différentes valeurs de k dans le calculateur RRF interactif.
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 classeurs 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, donnant 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 dans le classement. Le rang 1 obtient 1/201, le rang 10 obtient 1/210 — quasi identiques. À utiliser lorsque les classeurs individuels produisent des classements bruités mais que l’accord inter-listes est fiable.
Commencez avec k=60. L’article original sur RRF a montré que cette valeur est robuste sur des jeux de données TREC variés. Ne l’ajustez qu’après avoir mesuré les cas d’échec sur votre propre distribution de requêtes.
Résolution des égalités
Lorsque deux fragments 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 en :
- Préférant les fragments qui apparaissent dans les deux listes à ceux qui n’apparaissent que dans une seule
- Parmi les fragments présents dans les deux listes, préférant celui avec le rang combiné le plus bas
- Parmi les fragments présents dans une seule liste, préférant celui avec le rang 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 fragments 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 d’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 : les résultats sont ajoutés 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’erreurs. Le seul échec critique est l’absence du fichier de base de données.
Statistiques de production
Mesurées sur un coffre-fort de 16 894 fichiers, 49 746 fragments, une base de données SQLite de 83 Mo, Apple M3 Pro :
| Métrique | Valeur |
|---|---|
| Nombre total de fichiers | 16 894 |
| Nombre total de fragments | 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 |
| Durée de réindexation complète | ~4 minutes |
| Durée 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 été modifiés 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 fragment 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 hachage 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 original). Cependant, le hachage nécessite la lecture de chaque fichier à chaque exécution incrémentale. Pour 16 894 fichiers, la lecture du contenu prend 2 à 3 secondes. La lecture des mtime depuis le système de fichiers prend moins de 100 ms.
Le compromis : la comparaison de mtime déclenche occasionnellement 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 supprime tous ses fragments 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 supprimée. L’indexeur gère cela 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 d’eux, les garanties d’idempotence et la récupération après corruption.
Réindexation incrémentale
Quand l’utiliser : indexation quotidienne après modification de notes. C’est le mode par défaut.
Ce qu’il fait : 1. Analyse le vault à la recherche de fichiers modifiés (comparaison de mtime) 2. Supprime les chunks des fichiers supprimés 3. Re-découpe et ré-encode les fichiers modifiés 4. Insère de nouveaux chunks 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 vault de 16 000 fichiers.
python index_vault.py --incremental
Réindexation complète
Quand l’utiliser : - Après un changement de modèle d’embeddings (incompatibilité de hash du modèle détectée) - Après une migration de schéma (nouvelles colonnes, index modifiés) - Après une corruption de base de données (échec de la vérification d’intégrité) - Lorsque l’indexation incrémentale produit des résultats inattendus
Ce qu’il fait : 1. Supprime toutes les données existantes (chunks, vecteurs, entrées FTS5) 2. Analyse l’intégralité du vault 3. Découpe tous les fichiers en chunks 4. Encode tous les chunks en embeddings 5. Reconstruit l’index FTS5 à partir de 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 chunks existants d’un fichier avant d’insérer les 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 pleine 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 vault, 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.
Le flag --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 différence, 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. Re-découpe et ré-encode les 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 SIGINT en terminant le fichier en cours avant de s’arrêter.
Filtrage des identifiants et périmètre des données
Les notes personnelles contiennent des secrets : clés API, jetons bearer, chaînes de connexion à des 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 concernant 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 chunks, encodés en embeddings et stockés dans la base de données. Une recherche sur « authentication » renverrait le chunk contenant de vrais 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 chunk avant le stockage, en correspondant 25 motifs spécifiques à des 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 |
| Attributions 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 l’encodage. Le texte nettoyé est celui qui est encodé en embeddings. La représentation vectorielle n’encode jamais les motifs d’identifiants. Une requête sur « clé API » renvoie les notes qui traitent de la gestion des clés API, et non 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 était présent ici » sans encoder l’identifiant lui-même. -
Journaliser les motifs, pas les valeurs. Le filtre journalise 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 fournit une exclusion grossière par chemin. Le filtre d’identifiants fournit un nettoyage fin au sein des fichiers indexés. Les deux sont nécessaires :
.indexignorepour des 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 vaults 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) expose 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 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 s’exécute en tant que service HTTP autonome. Utile pour l’accès à distance, 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é (pas d’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 simultané au même coffre.
Conception des fonctionnalités
Le serveur MCP devrait exposer un ensemble minimal d’outils :
search — L’outil principal. Exécute la 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 chemin. Utile lorsque l’agent souhaite consulter 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 de commodité 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 devrait imposer 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 distincts, et non par le serveur MCP.
-
Limité au coffre. Le serveur ne lit que les fichiers situés dans le chemin de coffre configuré. Les tentatives de traversée de chemin (
../../etc/passwd) doivent être rejetées. -
Sortie filtrée des identifiants. 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. Imposez
max_tokenssur toutes les réponses des 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 de cycle de vie définis. 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 et les renvoie dans le coffre pour une recherche future.
#!/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 un frontmatter approprié 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 tenir compte de 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 l’en-tête de 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 sélectivement le contexte 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 d’édition en bénéficient.
Intégration avec Codex CLI
Codex CLI se connecte aux serveurs MCP via config.toml. Le pattern d’intégration diffère de Claude Code par la syntaxe de configuration et la méthode de transmission des instructions.
Configuration MCP
Ajoutez à .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"
Patterns AGENTS.md
Codex CLI lit AGENTS.md pour les instructions au niveau du projet. Incluez des directives de recherche dans le coffre :
## Outils disponibles
### Coffre-fort Obsidian (MCP : obsidian)
Utilisez l'outil `obsidian_search` pour trouver du contexte pertinent dans la base de connaissances.
Effectuez une recherche dans le coffre-fort lorsque vous avez besoin de :
- Contexte sur un concept ou un patron de conception
- Décisions antérieures ou justifications
- Documentation de référence pour l'implémentation
Exemples de requêtes :
- "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 patron 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-fort 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 les plus courants.
Cursor
Ajoutez ceci au fichier .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-fort :
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 la 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 moteur de recherche 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-fort 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 bien structurées du coffre-fort 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 (time-to-live). Lorsque le TTL expire, le bloc est reconstruit en interrogeant à nouveau le coffre-fort.
- Détection des modifications du coffre-fort. Lorsque l’indexeur détecte des modifications dans les fichiers ayant contribué à un bloc de contexte 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 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 en cache sont chargés au démarrage de la session. Les résultats de recherche dynamique remplissent le budget restant pour chaque requête. Cette approche hybride fournit à l’agent une base de contexte fréquemment nécessaire tout en préservant du budget pour les 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-fort, renvoyant 1 500 à 2 000 tokens de contexte. Sur 10 requêtes au cours d’une session, l’agent consomme 15 000 à 20 000 tokens de contexte issu du coffre-fort.
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 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 d’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 représente 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 produit 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 réussites/échecs, n’afficher que les échecs |
| Listes de fichiers | ls ou find dans la commande |
Tronquer aux 20 premières entrées + compteur |
| Traces de pile | Mot-clé Traceback |
Conserver la première et la dernière trame + message d’erreur |
| Statut Git | modified: / new file: |
Résumer les compteurs par statut |
| Sortie de build | warning: / error: |
Supprimer les lignes d’information, conserver avertissements/erreurs |
Pipeline d’intake et de triage des signaux
La couche d’intake détermine ce qui entre dans le vault. Sans curation, le vault accumule du bruit. Cette section couvre le pipeline de notation 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 enregistré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 : Résultats significatifs d’outils IA capturés via des hooks
Dimensions de notation
Chaque signal est évalué selon 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 | Le contenu est-il substantiel ? | Titres, résumé superficiel | Analyse détaillée avec exemples |
| Autorité | La source est-elle crédible ? | Blog anonyme, non vérifié | Source primaire, revue par les 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+ | Acheminement automatique vers le dossier de domaine |
| 0.40 - 0.55 | Mise en file d’attente pour revue manuelle |
| < 0.40 | Rejet (ne pas stocker) |
Routage par domaine
Les signaux dépassant 0.55 sont acheminés vers l’un des 12 dossiers de domaine en fonction de la correspondance par 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 des signaux traités | 7 771 |
| Acheminés automatiquement (>0.55) | 4 832 (62 %) |
| En file d’attente pour revue (0.40-0.55) | 1 543 (20 %) |
| Rejetés (<0.40) | 1 396 (18 %) |
| Dossiers de domaine actifs | 12 |
| Signaux moyens 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 du 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 sortants et les backlinks :
- Lien sortant : 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-sujet » | ”## 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 manières :
- Correspondance directe. Une recherche sur « authentication overview » correspond au MOC lui-même, fournissant à l’agent une liste organisée de notes associées.
- Expansion du contexte. Après avoir trouvé une note spécifique, le retriever 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 cartographie du sujet plus large.
Traversée de graphe pour l’expansion du contexte
Une amélioration future du retriever : 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)
Cette fonctionnalité n’est pas implémentée dans le retriever 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 vault. Le panneau de graphe dans Obsidian rend ces îlots déconnectés visibles. Les clusters orphelins indiquent des MOCs manquants ou des liens inter-domaines absents.
Prolifération de tags. Utilisation incohérente des tags ou création de tags trop granulaires. Un vault avec 500 tags uniques sur 5 000 notes affiche une moyenne d’une note pour 10 tags — les tags ne sont pas utiles pour le filtrage. Consolidez vers 20 à 50 tags de haut niveau correspondant à vos dossiers de domaine.
Notes riches en liens, pauvres en contenu. Notes constituées entièrement de wiki-links sans prose. Ces notes s’indexent mal car le chunker n’a aucun texte à convertir en embeddings. Ajoutez au minimum un paragraphe de contexte expliquant pourquoi les notes liées sont en rapport.
Liens bidirectionnels pour tout. Chaque référence ne nécessite pas un wiki-link. Mentionner « OAuth » en passant n’exige pas [[OAuth 2.0 Overview]]. Réservez les wiki-links aux relations intentionnelles et navigables où cliquer sur le lien fournirait un contexte utile.
Recettes de workflow pour développeurs
Workflows pratiques combinant la recherche dans le vault avec les tâches de développement quotidiennes.
Chargement du 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 retriever renvoie les notes récentes concernant votre projet actif, vous offrant un rappel rapide de l’état d’avancement. 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 les insights 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’insight capturé est immédiatement indexé 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é :
- Recherchez dans le vault : « Que sais-je sur [technologie/pattern] ? »
- Examinez les 5 premiers résultats pour les décisions antérieures et les pièges connus
- Vérifiez si un MOC existe pour le domaine ; sinon, créez-en un
- Recherchez les modes de défaillance : « problèmes avec [technologie] »
Débogage avec la recherche dans le vault
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. Cela s’avère particulièrement précieux pour les problèmes récurrents entre projets — le vault 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 vault renvoie les décisions antérieures, les contraintes architecturales et les standards de code pertinents pour le code en cours de revue. La revue s’appuie sur la connaissance institutionnelle, pas uniquement sur le diff.
Optimisation des performances
Cette section couvre les stratégies d’optimisation pour différentes tailles de coffre-fort et différents cas 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 de : - Augmenter la taille des lots de 64 à 128 pour accélérer la génération des embeddings - Utiliser le mode WAL (par défaut) pour les accès concurrents - Lancer 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 effectue 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’en ouvrir une nouvelle à chaque 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 données 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 de la requête et le formatage des résultats. La latence réseau pour la communication MCP STDIO ajoute 1 à 2 ms.
Dépannage
Dérive 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 l’horodatage de modification 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 non-correspondance du 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 différée), 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
Verrous de fichier SQLite
Symptôme : erreurs SQLITE_BUSY ou SQLITE_LOCKED.
Cause : plusieurs processus écrivent simultanément dans la base de données. Le mode WAL permet des lectures concurrentes mais n’autorise qu’un seul écrivain.
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, introuvable dans le chemin des bibliothèques, ou 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 dépassement de mémoire 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 tous les contenus de fichiers sont chargés 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 et génération des 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 contrôle — c’est du markdown standard - Notion inclut les tableaux de propriétés en HTML — convertissez-les en frontmatter YAML
- Notion intègre les images sous forme de 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 lots :
for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done - Déplacez dans le coffre-fort, ajoutez le frontmatter, organisez en dossiers
Depuis du markdown brut (sans Obsidian)
Si vous disposez déjà d’un répertoire de fichiers markdown :
- Ouvrez le répertoire en tant que coffre-fort Obsidian (Obsidian → Ouvrir un coffre-fort → Ouvrir un dossier)
- Ajoutez
.obsidian/à.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 fil 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 |
|---|---|
| 01 mars 2026 | Version initiale |
Références
-
Cormack, G.V., Clarke, C.L.A., et 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é d’un 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 plein texte avec classement BM25 et pondérations de colonnes configurables. ↩
-
sqlite-vec: A vector search SQLite extension. Fournit des tables virtuelles
vec0pour la recherche vectorielle KNN au sein de SQLite. ↩ -
Robertson, S. et 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. et 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 unimodales sur MS MARCO. ↩
-
SQLite Write-Ahead Logging. Le mode WAL pour des lectures concurrentes avec un seul écrivain. ↩
-
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(). ↩