obsidian:~/vault$ search --hybrid obsidian

Example vault location

#

words: 13314 read_time: 67m updated: 2026-03-05 07:41
$ retriever search --hybrid obsidian

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 :

  1. 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.

  2. 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.

  3. Une infrastructure communautaire. Découverte de plugins, marketplace de thèmes, service de synchronisation (optionnel), service de publication (optionnel) et un écosystème de documentation. Vous pouvez reproduire n’importe quelle fonctionnalité individuelle avec des outils autonomes, mais Obsidian les regroupe dans un workflow cohérent.

Ce qu’Obsidian ne fait PAS (et ce que vous construisez)

Obsidian n’inclut pas d’infrastructure de recherche. Il dispose d’une recherche basique (plein texte, nom de fichier, tag) mais pas de pipeline d’embedding, pas de recherche vectorielle, pas de classement par fusion, pas de serveur MCP, pas de filtrage des identifiants, pas de stratégie de découpage (chunking), et pas de hooks d’intégration pour les outils IA externes. Ce guide couvre l’infrastructure que vous construisez au-dessus d’Obsidian. Le coffre est le substrat. Le pipeline de recherche, le serveur MCP et les hooks d’intégration sont l’infrastructure.

L’architecture décrite ici est markdown-first, pas exclusive à Obsidian. Si vous utilisez Logseq, Foam, Dendron ou un simple répertoire de fichiers markdown, le pipeline de recherche fonctionne de manière identique. Le découpeur lit les fichiers .md. L’encodeur traite des chaînes de texte. L’indexeur écrit dans SQLite. Aucun de ces composants ne dépend de fonctionnalités spécifiques à Obsidian. La contribution d’Obsidian est l’environnement d’écriture et d’organisation qui produit les fichiers markdown que le système de recherche indexe.


Démarrage rapide : votre premier coffre-fort connecté à l’IA

Cette section vous permet de connecter un coffre-fort à un outil d’IA en cinq minutes. Vous allez installer Obsidian, créer un coffre-fort, installer un serveur MCP, et exécuter votre première requête. Ce démarrage rapide utilise un serveur MCP communautaire pour obtenir des résultats immédiats. Les sections suivantes couvrent la construction d’un pipeline de recherche personnalisé pour un usage en production.

Prérequis

  • macOS, Linux ou Windows
  • Node.js 18+ (pour le serveur MCP)
  • Claude Code, Codex CLI, ou Cursor installé

Étape 1 : Créer un coffre-fort

Téléchargez Obsidian depuis obsidian.md et créez un nouveau coffre-fort. Choisissez un emplacement dont vous vous souviendrez — le serveur MCP a besoin du chemin absolu.

# Example vault location
~/Documents/knowledge-base/

Ajoutez quelques notes pour donner au système de recherche du contenu exploitable. Même 10 à 20 notes suffisent pour observer des résultats. Chaque note doit être un fichier .md avec un titre significatif et au moins un paragraphe de contenu.

Étape 2 : Installer un serveur MCP

Plusieurs serveurs MCP communautaires offrent un accès immédiat au coffre-fort. L’écosystème s’est considérablement développé entre 2025 et 2026 :

Serveur Auteur Transport Nécessite un plugin Fonctionnalité clé
obsidian-mcp-server StevenStavrakis STDIO Non Léger, basé sur les fichiers
mcp-obsidian MarkusPfundstein STDIO Local REST API CRUD complet du coffre-fort via REST
obsidian-mcp-tools jacksteamdev STDIO Oui (plugin) Recherche sémantique + Templater
obsidian-claude-code-mcp iansinnott WebSocket Oui (plugin) Auto-découverte pour Claude Code
obsidian-mcp-server cyanheads STDIO Local REST API Tags, gestion du frontmatter

Pour ce démarrage rapide, l’option la plus simple est un serveur basé sur les fichiers qui lit directement les fichiers .md :

npm install -g obsidian-mcp-server

Étape 3 : Configurer votre outil d’IA

Claude Code — ajoutez dans ~/.claude/settings.json :

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

Codex CLI — ajoutez dans .codex/config.toml :

[mcp_servers.obsidian]
command = "obsidian-mcp-server"
args = ["--vault", "/absolute/path/to/your/vault"]

Cursor — ajoutez dans .cursor/mcp.json :

{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mcp-server",
      "args": ["--vault", "/absolute/path/to/your/vault"]
    }
  }
}

Étape 4 : Exécuter votre première requête

Ouvrez votre outil d’IA et posez une question à laquelle vos notes de coffre-fort peuvent répondre :

Search my Obsidian vault for notes about [topic you wrote about]

L’outil d’IA appelle le serveur MCP, qui effectue une recherche dans votre coffre-fort et renvoie le contenu correspondant. Vous devriez voir des résultats avec les chemins de fichiers et des extraits pertinents.

Ce que vous venez de construire

Vous avez connecté une base de connaissances locale à un outil d’IA via un protocole standard. Le serveur MCP lit les fichiers de votre coffre-fort, effectue une recherche basique et renvoie les résultats. Il s’agit de la version minimale viable.

Ce que ce démarrage rapide ne vous apporte PAS : - La recherche hybride (BM25 + recherche vectorielle + fusion RRF) - La recherche sémantique par embeddings - Le filtrage des identifiants sensibles - L’indexation incrémentale - L’injection automatique de contexte via des hooks

Le reste de ce guide couvre la construction de chacune de ces fonctionnalités. Le démarrage rapide valide le concept. Le pipeline complet offre une recherche de qualité production.


Cadre de décision : Obsidian face aux alternatives

Tous les cas d’usage ne nécessitent pas Obsidian. Cette section identifie quand Obsidian est le bon substrat, quand c’est excessif, et quand une autre solution convient mieux.

Arbre de décision

START: What is your primary content type?

├─ Structured data (tables, records, schemas)
   Use a database. SQLite, PostgreSQL, or a spreadsheet.
   Obsidian is for prose, not tabular data.

├─ Ephemeral context (current project, temporary notes)
   Use CLAUDE.md / AGENTS.md in the project repo.
   These travel with the code and reset per project.

├─ Team wiki (shared documentation, onboarding)
   Evaluate Notion, Confluence, or a shared git repo.
   Obsidian vaults are personal-first. Team sync is possible
    but not native.

└─ Growing personal knowledge corpus
   
   ├─ < 50 notes
      A folder of markdown files + grep is sufficient.
      Obsidian adds value mainly through the link graph,
       which needs density to be useful.
   
   ├─ 50 - 500 notes
      Obsidian adds value. Wiki-links create a navigable graph.
      BM25-only search (FTS5) is sufficient at this scale.
      Skip vector search and RRF until keyword collisions appear.
   
   ├─ 500 - 5,000 notes
      Full hybrid retrieval becomes valuable. Keyword collisions
       increase. Semantic search catches queries that BM25 misses.
      Add vector search + RRF fusion at this scale.
   
   └─ 5,000+ notes
       Full pipeline is essential. BM25-only returns too much noise.
       Credential filtering becomes critical (more notes = more
        accidentally pasted secrets).
       Incremental indexing matters (full reindex takes minutes).
       MCP integration pays dividends on every AI interaction.

Matrice de comparaison

Critère Obsidian Notion Apple Notes Système de fichiers CLAUDE.md
Local d’abord Oui Non (cloud) Partiel (iCloud) Oui Oui
Texte brut Oui (markdown) Non (blocs) Non (propriétaire) Oui Oui
Structure en graphe Oui (wiki-links) Partiel (mentions) Non Non Non
Indexable par l’IA Accès direct aux fichiers API requise Export nécessaire Accès direct aux fichiers Déjà dans le contexte
Écosystème de plugins 1 800+ plugins Intégrations Aucun N/A N/A
Utilisable hors ligne Complet Lecture seule en cache Partiel Complet Complet
Supporte 10 000+ notes Oui Oui (avec API) Se dégrade Oui Non (fichier unique)
Coût Gratuit (core) 10 $/mois+ Gratuit Gratuit Gratuit

Quand Obsidian est excessif

  • Contexte limité à un seul projet. Si l’IA n’a besoin que du contexte du code en cours, placez-le dans CLAUDE.md, AGENTS.md, ou la documentation au niveau du projet. Ces fichiers accompagnent le dépôt et sont chargés automatiquement.
  • Données structurées. Si le contenu est constitué de tableaux, d’enregistrements ou de schémas, utilisez une base de données. Les notes Obsidian sont conçues pour la prose. Dataview peut interroger les champs du frontmatter, mais une vraie base de données gère mieux les requêtes structurées.
  • Recherche temporaire. Si les notes seront supprimées à la fin du projet, un répertoire de travail avec des fichiers markdown est plus simple. Ne construisez pas une infrastructure de recherche pour du contenu éphémère.

Quand Obsidian est le bon choix

  • Accumulation de connaissances sur des mois ou des années. La valeur se compose à mesure que le corpus grandit. Un coffre-fort de 200 notes interrogé quotidiennement pendant six mois apporte plus de valeur qu’un coffre-fort de 5 000 notes interrogé une seule fois.
  • Plusieurs domaines dans un même corpus. Un coffre-fort contenant des notes sur la programmation, l’architecture, la sécurité, le design et des projets personnels bénéficie d’une recherche inter-domaines qu’un CLAUDE.md spécifique à un projet ne peut pas offrir.
  • Contenu sensible en matière de confidentialité. Le fonctionnement local signifie que le pipeline de recherche n’envoie jamais de contenu vers des services externes. Le coffre-fort contient tout ce que vous y mettez, y compris du contenu que vous ne téléverseriez pas sur un service cloud.

Modèle mental : trois couches

Le système comporte trois couches qui fonctionnent indépendamment mais dont les effets se multiplient lorsqu’elles sont combinées. Chaque couche a une préoccupation différente et un mode de défaillance différent.

┌─────────────────────────────────────────────────────┐
                 INTEGRATION LAYER                     
  MCP servers, hooks, skills, context injection        
  Concern: delivering context to AI tools              
  Failure: wrong context, too much context, stale      
└──────────────────────┬──────────────────────────────┘
                        query + ranked results
┌──────────────────────┴──────────────────────────────┐
                  RETRIEVAL LAYER                      
  BM25, vector KNN, RRF fusion, token budget           
  Concern: finding the right content for any query     
  Failure: wrong ranking, missed results, slow queries 
└──────────────────────┬──────────────────────────────┘
                        chunked, embedded, indexed
┌──────────────────────┴──────────────────────────────┐
                   INTAKE LAYER                        
  Note creation, signal triage, vault organization     
  Concern: what enters the vault and how it's stored   │
  Failure: noise, duplicates, missing structure        
└─────────────────────────────────────────────────────┘

L’ingestion détermine ce qui entre dans le coffre-fort. Sans curation, le coffre-fort accumule du bruit : captures d’écran de tweets, articles copiés-collés sans annotation, réflexions inachevées sans contexte. La couche d’ingestion est responsable du contrôle qualité au point d’entrée. Un pipeline de scoring, une convention de balisage ou un processus de révision manuelle — tout mécanisme qui garantit que le coffre-fort contient du contenu qui mérite d’être retrouvé.

La recherche rend le coffre-fort interrogeable. C’est le moteur : découpage des notes en unités de recherche, transformation des segments en vecteurs (embeddings), indexation pour la recherche par mots-clés et sémantique, fusion des résultats avec RRF. La couche de recherche transforme un répertoire de fichiers en une base de connaissances interrogeable. Sans cette couche, le coffre-fort est navigable par exploration manuelle et recherche basique, mais n’est pas accessible programmatiquement par les outils d’IA.

L’intégration connecte la couche de recherche aux outils d’IA. Un serveur MCP expose la recherche comme un outil appelable. Les hooks injectent du contexte automatiquement. Les skills capturent de nouvelles connaissances dans le coffre-fort. La couche d’intégration est l’interface entre la base de connaissances et les agents d’IA qui la consomment.

Les couches sont découplées par conception. Le pipeline de scoring d’ingestion ne sait rien des embeddings. Le moteur de recherche ne sait rien des règles de routage des signaux. Le serveur MCP ne sait rien de la manière dont les notes ont été créées. Ce découplage signifie que vous pouvez améliorer chaque couche indépendamment. Remplacez le modèle d’embedding sans modifier le pipeline d’ingestion. Ajoutez une nouvelle capacité MCP sans modifier le moteur de recherche. Changez les heuristiques de scoring des signaux sans toucher à l’index.


Architecture du coffre-fort pour la consommation par l’IA

Un coffre-fort optimisé pour la recherche par IA suit des conventions différentes de celles d’un coffre-fort optimisé pour la navigation personnelle. Cette section couvre la structure des dossiers, le schéma des notes, les conventions de frontmatter et les patterns spécifiques qui améliorent la qualité de la recherche.

Structure des dossiers

Utilisez des préfixes numérotés pour les dossiers de premier niveau afin de créer une hiérarchie organisationnelle prévisible. Les numéros n’impliquent pas de priorité — ils regroupent des domaines liés et rendent la structure facile à parcourir.

vault/
├── 00-inbox/              # Unsorted captures, pending triage
├── 01-projects/           # Active project notes
├── 02-areas/              # Ongoing areas of responsibility
├── 03-resources/          # Reference material by topic
   ├── programming/
   ├── security/
   ├── ai-engineering/
   ├── design/
   └── devops/
├── 04-archive/            # Completed projects, old references
├── 05-signals/            # Scored signal intake
   ├── ai-tooling/
   ├── security/
   ├── systems/
   └── ...12 domain folders
├── 06-daily/              # Daily notes (if used)
├── 07-templates/          # Note templates (excluded from index)
├── 08-attachments/        # Images, PDFs (excluded from index)
├── .obsidian/             # Obsidian config (excluded from index)
└── .indexignore            # Paths to exclude from retrieval index

Dossiers à indexer : tout ce qui contient de la prose en markdown — projets, domaines de responsabilité, ressources, signaux, notes quotidiennes.

Dossiers à exclure de l’indexation : les templates (ils contiennent des variables de substitution, pas du contenu), les pièces jointes (fichiers binaires), la configuration Obsidian, et tout dossier contenant du contenu sensible que vous ne souhaitez pas inclure dans l’index de recherche.

Le fichier .indexignore

Créez un fichier .indexignore à la racine du coffre-fort pour exclure explicitement des chemins de l’index de recherche. La syntaxe est identique à celle de .gitignore :

# Obsidian internal
.obsidian/

# Templates contain placeholders, not content
07-templates/

# Binary attachments
08-attachments/

# Personal health/medical notes
02-areas/health/

# Financial records
02-areas/finance/personal/

# Career documents (resumes, salary data)
02-areas/career/private/

L’indexeur lit ce fichier avant l’analyse et ignore entièrement les chemins correspondants. Les fichiers dans les chemins exclus ne sont jamais découpés, jamais transformés en vecteurs et n’apparaissent jamais dans les résultats de recherche.

Schéma des notes

Chaque note devrait avoir un frontmatter YAML. Le moteur de recherche utilise les champs du frontmatter pour le filtrage et l’enrichissement du contexte :

---
title: "OAuth Token Rotation Patterns"
type: note           # note | signal | project | moc | daily
domain: security     # primary domain for routing
tags:
  - authentication
  - oauth
  - token-management
created: 2026-01-15
updated: 2026-02-28
source: ""           # URL if captured from external source
status: active       # active | archived | draft
---

Champs requis pour la recherche :

  • title — Utilisé dans l’affichage des résultats de recherche et le contexte des en-têtes pour BM25
  • type — Permet des requêtes filtrées par type (« afficher uniquement les MOCs » ou « uniquement les signaux »)
  • tags — Indexés dans le contexte des en-têtes FTS5 avec un poids de 0.3, fournissant des correspondances par mots-clés même lorsque le corps utilise une terminologie différente

Champs optionnels mais précieux :

  • domain — Permet des requêtes limitées à un domaine (« rechercher uniquement dans les notes de sécurité »)
  • source — Attribution pour le contenu capturé ; le moteur de recherche peut inclure les URLs sources dans les résultats
  • status — Permet d’exclure les notes archivées ou en brouillon de la recherche active

Conventions de découpage

Le moteur de recherche découpe au niveau des en-têtes H2 (##). Cela signifie que la structure de vos notes affecte directement la granularité de la recherche :

Bon pour la recherche :

## Token Rotation Strategy

The rotation interval depends on the threat model...

## Implementation with refresh_token

The OAuth 2.0 refresh token flow requires...

## Error Handling: Expired Tokens

When a token expires mid-request...

Trois sections H2 produisent trois segments interrogeables indépendamment. Chaque segment contient suffisamment de contexte pour que l’embedding capture son sens. Une requête sur « la gestion des tokens expirés » correspond spécifiquement au troisième segment.

Mauvais pour la recherche :

# OAuth Notes

Token rotation depends on threat model. The OAuth 2.0 refresh
token flow requires storing the refresh token securely. When a
token expires mid-request, the client should retry after refresh.
The rotation interval is typically 15-30 minutes for access tokens
and 7-30 days for refresh tokens...

Une longue section sans en-têtes H2 produit un seul segment volumineux. L’embedding fait la moyenne de tous les sujets de la section. Une requête sur n’importe quel sous-thème correspond de manière égale à la note entière.

Règle empirique : si une section couvre plus d’un concept, divisez-la en sous-sections H2. Le découpeur se charge du reste.

Ce qu’il ne faut pas mettre dans les notes

Contenu qui dégrade la qualité de la recherche :

  • Copier-coller brut d’articles entiers sans annotation. Le moteur de recherche indexe les mots-clés de l’article original, diluant votre coffre-fort avec du contenu que vous n’avez pas écrit. Ajoutez un résumé, extrayez les points clés ou créez un lien vers l’URL source à la place.
  • Captures d’écran sans description textuelle. Le moteur de recherche indexe le texte markdown. Une image sans texte alternatif ni description environnante est invisible tant pour BM25 que pour la recherche vectorielle.
  • Chaînes d’identifiants. Clés API, tokens, mots de passe, chaînes de connexion. Même avec le filtrage des identifiants, l’approche la plus sûre est de ne jamais coller de secrets dans les notes. Référencez-les par leur nom (« le token API Cloudflare dans ~/.env ») à la place.
  • Contenu généré automatiquement sans curation. Si un outil génère une note (transcription de réunion, surlignages Readwise, import RSS), relisez-la et annotez-la avant qu’elle n’entre dans le coffre-fort permanent. Les imports automatiques non révisés ajoutent du volume sans ajouter de valeur recherchable.

Écosystème de plugins pour les workflows IA

Les plugins Obsidian qui améliorent la qualité du vault pour la récupération IA se répartissent en trois catégories : structurels (imposent la cohérence), requêtes (exposent les métadonnées) et synchronisation (maintiennent le vault à jour).

Plugins essentiels

Dataview. Interroge votre vault comme une base de données à l’aide des champs frontmatter. Créez des index dynamiques : « toutes les notes avec le tag security mises à jour au cours des 30 derniers jours » ou « toutes les notes de projet avec le statut active ». Dataview n’aide pas directement la récupération, mais il vous permet d’identifier les lacunes dans la couverture de votre vault et de trouver les notes nécessitant une mise à jour.

TABLE type, domain, updated
FROM "03-resources"
WHERE status = "active"
SORT updated DESC
LIMIT 20

Templater. Crée des notes à partir de modèles avec des champs dynamiques. Assurez-vous que chaque nouvelle note commence avec un frontmatter correct en utilisant un modèle qui pré-remplit les champs created, type et domain. Un frontmatter cohérent améliore le filtrage lors de la récupération.

<%* /* New Resource Note Template */ %>
---
title: "<% tp.file.cursor() %>"
type: note
domain: <% tp.system.suggester(["programming", "security", "ai-engineering", "design", "devops"], ["programming", "security", "ai-engineering", "design", "devops"]) %>
tags: []
created: <% tp.date.now("YYYY-MM-DD") %>
updated: <% tp.date.now("YYYY-MM-DD") %>
source: ""
status: active
---

## Key Points

## Details

## References

Linter. Applique des règles de formatage à l’ensemble du vault. Une hiérarchie de titres cohérente (H1 pour le titre, H2 pour les sections, H3 pour les sous-sections) garantit que le chunker produit des résultats prévisibles. Règles du Linter importantes pour la récupération :

  • Incrémentation des titres : imposer des niveaux de titre séquentiels (pas de saut de H1 à H3)
  • YAML title : correspondre au nom de fichier
  • Espaces en fin de ligne : supprimer (évite les artefacts de tokenisation FTS5)
  • Lignes vides consécutives : limiter à 1 (chunks plus propres)

Intégration Git. Contrôle de version pour votre vault. Suivez les modifications au fil du temps, synchronisez entre plusieurs machines et récupérez les suppressions accidentelles. Git fournit également les données mtime que l’indexeur utilise pour la détection incrémentale des changements.

Plugins qui facilitent l’indexation

Smart Connections. Un plugin Obsidian qui offre une recherche sémantique alimentée par l’IA directement dans Obsidian. Smart Connections v4 crée des embeddings locaux par défaut — une fois votre vault indexé, les connexions sémantiques et la recherche fonctionnent entièrement hors ligne sans aucun appel API.21 Bien que le système de récupération présenté dans ce guide soit externe à Obsidian (il s’exécute comme un pipeline Python), Smart Connections est utile pour explorer les relations sémantiques pendant la rédaction. Les deux systèmes indexent le même contenu mais répondent à des cas d’usage différents : Smart Connections pour la découverte dans l’éditeur, le récupérateur externe pour l’intégration des outils IA via MCP.

Metadata Menu. Offre une édition structurée du frontmatter avec autocomplétion des valeurs de champs. Réduit les fautes de frappe dans les champs type, domain et tags. Des métadonnées cohérentes améliorent la précision du filtrage lors de la récupération.

Plugins qui nuisent à l’indexation

Excalidraw. Stocke les dessins sous forme de JSON intégré dans des fichiers markdown. Le JSON est syntaxiquement du markdown valide mais produit des résultats incohérents lorsqu’il est découpé en chunks et transformé en embeddings. Excluez les fichiers Excalidraw de l’index via .indexignore ou filtrez par extension de fichier.

Kanban. Stocke l’état du tableau sous forme de markdown spécialement formaté. Le format est conçu pour le rendu Kanban, pas pour la récupération de texte. Le chunker produit des fragments de titres de cartes et de métadonnées qui ne se prêtent pas bien aux embeddings. Excluez les tableaux Kanban de l’index.

Calendar. Crée des notes quotidiennes avec un contenu minimal (souvent juste un en-tête de date). Les notes vides ou quasi vides produisent des chunks de faible qualité. Si vous utilisez des notes quotidiennes, rédigez-y du contenu substantiel ou excluez le dossier des notes quotidiennes de l’index.

Configuration des plugins qui compte

File recovery → Activé. Protège contre la suppression accidentelle de notes. Pas directement lié à la récupération, mais essentiel pour une base de connaissances sur laquelle vous comptez.

Strict line breaks → Désactivé. Les sauts de ligne standard du markdown (double retour à la ligne pour un paragraphe) produisent des chunks plus propres que le mode strict d’Obsidian (simple retour à la ligne pour <br>).

Default new file location → Dossier désigné. Dirigez les nouveaux fichiers vers 00-inbox/ afin que les notes non catégorisées ne polluent pas les dossiers de domaine. La boîte de réception est une zone de transit ; les fichiers sont déplacés vers les dossiers de domaine après le tri.

Wiki-link format → Chemin le plus court si possible. Des cibles de liens plus courtes sont plus faciles à résoudre pour le récupérateur lors de l’indexation de la structure des liens.


Modèles d’embeddings : choix et configuration

Le modèle d’embedding convertit les fragments de texte en vecteurs numériques pour la recherche sémantique. Le choix du modèle détermine la qualité de la recherche, la taille de l’index, la vitesse d’embedding et les dépendances à l’exécution. Cette section explique pourquoi potion-base-8M de Model2Vec est le choix par défaut et dans quels cas privilégier des alternatives.

Pourquoi Model2Vec potion-base-8M

Modèle : minishlab/potion-base-8M Paramètres : 7,6 millions Dimensions : 256 Taille : ~30 Mo Dépendances : model2vec (numpy uniquement, sans PyTorch) Inférence : CPU uniquement, embeddings statiques de mots (sans couches d’attention)

Model2Vec distille les connaissances d’un sentence transformer en embeddings statiques de tokens. Au lieu d’exécuter des couches d’attention sur l’entrée (comme le font BERT, MiniLM et les autres modèles transformer), Model2Vec produit des vecteurs par moyenne pondérée d’embeddings de tokens précalculés.3 La conséquence pratique : la vitesse d’embedding est 50 à 500 fois supérieure à celle des modèles basés sur des transformers, car il n’y a aucun calcul séquentiel.

Sur la suite de benchmarks MTEB, potion-base-8M atteint 89 % des performances de all-MiniLM-L6-v2 (50,03 contre 56,09 en moyenne).4 L’écart de qualité de 11 % est le compromis accepté en échange des avantages de vitesse et de simplicité. Pour des fragments markdown courts (200 à 400 mots en moyenne dans un coffre-fort typique), la différence de qualité est moins marquée que sur des documents longs, car les deux modèles convergent vers des représentations similaires pour des textes courts et ciblés.

Configuration

# embedder.py
DEFAULT_MODEL = "minishlab/potion-base-8M"
EMBEDDING_DIM = 256

class Model2VecEmbedder:
    def __init__(self, model_name=DEFAULT_MODEL):
        self._model_name = model_name
        self._model = None

    def _ensure_model(self):
        if self._model is not None:
            return
        _activate_venv()  # Add isolated venv to sys.path
        from model2vec import StaticModel
        self._model = StaticModel.from_pretrained(self._model_name)

    def embed_batch(self, texts):
        self._ensure_model()
        vecs = self._model.encode(texts)
        return [v.tolist() for v in vecs]

Chargement différé. Le modèle se charge à la première utilisation, pas à l’importation. Importer le module d’embedding ne coûte rien lorsque le système de recherche fonctionne en mode de repli BM25 uniquement (par exemple, lorsque l’environnement virtuel d’embedding n’est pas installé).

Environnement virtuel isolé. Le modèle s’exécute dans un venv dédié (par exemple ~/.claude/venvs/memory/) afin d’éviter les conflits de dépendances avec le reste de la chaîne d’outils. La fonction _activate_venv() ajoute le répertoire site-packages du venv au sys.path à l’exécution.

# Create isolated venv
python3 -m venv ~/.claude/venvs/memory
~/.claude/venvs/memory/bin/pip install model2vec

Traitement par lots. L’embedder traite les textes par lots de 64 pour amortir la surcharge de Model2Vec. L’indexeur transmet les fragments à embed_batch() plutôt que de les encoder un par un.

Quand choisir des alternatives

Modèle Dim Taille Vitesse Qualité (MTEB) Idéal pour
potion-base-8M 256 30 Mo 500x 50,03 Par défaut : local, rapide, sans GPU
potion-base-32M 256 120 Mo 400x 52,46 Meilleure qualité, toujours statique
potion-retrieval-32M 256 120 Mo 400x 36,35 (recherche) Statique optimisé pour la recherche
all-MiniLM-L6-v2 384 80 Mo 1x 56,09 Meilleure qualité, toujours local
nomic-embed-text-v1.5 768 270 Mo 0,5x 62,28 Meilleure qualité locale
text-embedding-3-small 1536 API N/A 62,30 Basé sur API, qualité maximale

Choisissez potion-base-32M si vous souhaitez une meilleure qualité que potion-base-8M sans quitter la famille des embeddings statiques. Publié en janvier 2025, il utilise un vocabulaire plus large distillé à partir de baai/bge-base-en-v1.5, atteignant 52,46 de moyenne MTEB (amélioration de 5 % par rapport à potion-base-8M) tout en conservant la même sortie en 256 dimensions et la dépendance numpy uniquement.18 Le fichier modèle 4 fois plus volumineux augmente l’utilisation mémoire, mais la vitesse d’embedding reste de plusieurs ordres de grandeur supérieure à celle des modèles transformer.

Choisissez potion-retrieval-32M lorsque votre cas d’utilisation principal est la recherche (ce qui est le cas pour la recherche dans un coffre-fort). Cette variante est affinée (fine-tuned) à partir de potion-base-32M spécifiquement pour les tâches de recherche, obtenant 36,35 sur les benchmarks de recherche MTEB contre 33,52 pour le modèle de base.18 La moyenne MTEB globale descend à 49,73 car l’affinage sacrifie les performances générales au profit de gains spécifiques à la recherche.

Choisissez all-MiniLM-L6-v2 lorsque la qualité de recherche prime sur la vitesse et que PyTorch est installé. Les vecteurs en 384 dimensions augmentent la taille de la base SQLite d’environ 50 % par rapport aux vecteurs en 256 dimensions. La vitesse d’embedding passe de moins d’une minute à environ 10 minutes pour une réindexation complète de 15 000 fichiers sur du matériel Apple Silicon (série M).

Choisissez nomic-embed-text-v1.5 lorsque vous avez besoin de la meilleure qualité de recherche locale possible et acceptez une indexation plus lente. Les vecteurs en 768 dimensions triplent approximativement la taille de la base de données. Nécessite PyTorch et un processeur moderne ou un GPU.

Choisissez text-embedding-3-small lorsque la latence réseau et la confidentialité sont des compromis acceptables. API produit les embeddings de la plus haute qualité, mais introduit une dépendance au cloud, un coût par token (0,02 $/million de tokens) et envoie votre contenu aux serveurs d’OpenAI.

Restez sur potion-base-8M dans tous les autres cas. L’avantage de vitesse est crucial pour l’indexation itérative (réindexation pendant le développement), la dépendance numpy uniquement évite la complexité d’installation de PyTorch, et les vecteurs en 256 dimensions maintiennent une base de données compacte.

Quantification et réduction de dimensionnalité

Model2Vec v0.5.0+ prend en charge le chargement de modèles avec une précision et des dimensions réduites.18 Cela s’avère utile pour le déploiement sur du matériel contraint ou pour réduire la taille de la base de données sans changer de modèle :

from model2vec import StaticModel

# Load with int8 quantization (25% of original size)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", quantize=True)

# Load with reduced dimensions (e.g., 128 instead of 256)
model = StaticModel.from_pretrained("minishlab/potion-base-8M", dimensionality=128)

Les modèles quantifiés conservent une qualité de recherche quasi identique pour une fraction de l’empreinte mémoire. La réduction de dimensionnalité suit une troncature de type Matryoshka — les N premières dimensions portent le plus d’information. Passer de 256 à 128 dimensions divise par deux le stockage des vecteurs avec une perte de qualité minimale pour la recherche sur des textes courts.

Depuis mai 2025, Model2Vec prend également en charge les tokenizers BPE et Unigram (en plus de WordPiece), ce qui élargit l’ensemble des sentence transformers pouvant être distillés en modèles statiques.20

Affinage pour des embeddings spécifiques au coffre-fort

Model2Vec v0.4.0+ prend en charge l’entraînement de modèles de classification personnalisés par-dessus les embeddings statiques, et la version 0.7.0 ajoute la quantification du vocabulaire et le pooling configurable pour la distillation.20 Ceci est pertinent pour les coffres-forts au vocabulaire spécialisé (notes médicales, références juridiques, jargon propre à un domaine) où les modèles potion par défaut pourraient ne pas capturer les nuances sémantiques :

from model2vec import StaticModel
from model2vec.train import train_model

# Fine-tune on vault-specific data
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
trained_model = train_model(model, train_texts, train_labels)
trained_model.save_pretrained("./vault-embeddings")

Pour la plupart des coffres-forts, potion-base-8M par défaut produit une qualité de recherche suffisante. L’affinage ne se justifie que lorsque la recherche manque systématiquement des connexions spécifiques au domaine qu’un modèle généraliste ne peut pas capturer.

Suivi du hash du modèle

L’indexeur stocke un hash dérivé du nom du modèle et de la taille du vocabulaire. Si vous changez de modèle d’embedding, l’indexeur détecte l’incompatibilité lors de la prochaine exécution incrémentale et déclenche automatiquement une réindexation complète.

def _compute_model_hash(self):
    """Hash model name + vocab size for compatibility tracking."""
    key = f"{self._model_name}:{self._model.vocab_size}"
    return hashlib.sha256(key.encode()).hexdigest()[:16]

Cela empêche de mélanger des vecteurs provenant de modèles différents dans la même base de données, ce qui produirait des scores de cosine similarity incohérents.

Modes de défaillance

Échec du téléchargement du modèle. La première exécution télécharge le modèle depuis Hugging Face. Si le téléchargement échoue (problème réseau, pare-feu d’entreprise), le système de recherche bascule en mode BM25 uniquement. Le modèle est mis en cache localement après le premier téléchargement.

Incompatibilité de dimensions. Si vous changez de modèle sans vider la base de données, les vecteurs stockés ont une dimension différente de celle des nouveaux embeddings. L’indexeur détecte cela via le hash du modèle et déclenche une réindexation complète. Si la vérification du hash échoue (modèle personnalisé sans hash approprié), sqlite-vec renverra une erreur sur les requêtes KNN en raison de l’incompatibilité de dimensions.

Pression mémoire sur les grands coffres-forts. Encoder plus de 50 000 fragments en un seul lot peut consommer une quantité significative de mémoire. L’indexeur traite par lots de 64 pour limiter le pic d’utilisation mémoire. Si la mémoire pose toujours problème, réduisez la taille des lots.


Recherche plein texte avec FTS5

L’extension FTS5 de SQLite fournit une recherche plein texte avec classement BM25. FTS5 est le composant de recherche par mots-clés du pipeline de récupération hybride (hybrid retrieval). Cette section couvre la configuration de FTS5, les cas où BM25 excelle et ses modes de défaillance spécifiques.

Table virtuelle FTS5

CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text,
    section,
    heading_context,
    content=chunks,
    content_rowid=id
);

Mode content-sync. Le paramètre content=chunks indique à FTS5 de référencer directement la table chunks plutôt que de stocker une copie dupliquée du texte. Cela réduit de moitié l’espace de stockage requis, mais implique que FTS5 doit être synchronisé manuellement lors de l’insertion, de la mise à jour ou de la suppression de fragments (chunks).

Colonnes. Trois colonnes sont indexées : - chunk_text — Le contenu principal de chaque fragment (poids BM25 : 1.0) - section — Le texte du titre H2 (poids BM25 : 0.5) - heading_context — Titre de la note, tags et métadonnées (poids BM25 : 0.3)

Classement BM25

BM25 classe les documents selon la fréquence des termes, la fréquence inverse des documents et la normalisation par longueur de document. La fonction auxiliaire bm25() de FTS5 accepte des poids par colonne :

SELECT
    c.id, c.file_path, c.section, c.chunk_text,
    bm25(chunks_fts, 1.0, 0.5, 0.3) AS score
FROM chunks_fts
JOIN chunks c ON chunks_fts.rowid = c.id
WHERE chunks_fts MATCH ?
ORDER BY score
LIMIT 30;

Les poids des colonnes (1.0, 0.5, 0.3) signifient : - Une correspondance de mot-clé dans chunk_text contribue le plus au score - Une correspondance dans section (titre) contribue moitié moins - Une correspondance dans heading_context (titre, tags) contribue à hauteur de 30 %

Ces poids sont ajustables. Si votre coffre-fort (vault) contient des titres descriptifs qui prédisent fortement la qualité du contenu, augmentez le poids de section. Si vos tags sont complets et précis, augmentez le poids de heading_context.

Quand BM25 l’emporte

BM25 excelle pour les requêtes contenant des identifiants exacts :

  • Noms de fonctions : _rrf_fuse, embed_batch, get_stale_files
  • CLI flags : --incremental, --vault, --model
  • Clés de configuration : bm25_weight, max_tokens, batch_size
  • Messages d’erreur : SQLITE_LOCKED, ConnectionRefusedError
  • Termes techniques spécifiques : PostToolUse, PreToolUse, AGENTS.md

Pour ces requêtes, BM25 trouve la correspondance exacte immédiatement. La recherche vectorielle renverrait du contenu sémantiquement lié mais pourrait classer la correspondance exacte plus bas qu’une discussion conceptuelle.

Quand BM25 échoue

BM25 échoue pour les requêtes qui utilisent une terminologie différente de celle du contenu stocké :

  • Requête : « how to handle authentication failures » → Le coffre-fort contient des notes sur « login error recovery » et « session expiration handling ». BM25 ne trouve pas de correspondance car les mots-clés diffèrent.
  • Requête : « what is the best way to manage state » → Le coffre-fort contient des notes sur « Redux store patterns » et « context providers ». BM25 échoue car la « gestion d’état » est exprimée à travers des noms de technologies spécifiques.

BM25 échoue également en cas de collision de mots-clés à grande échelle. Dans un coffre-fort de 15 000 fichiers, une recherche sur « configuration » correspond à des centaines de notes car presque chaque note de projet mentionne la configuration. Les résultats sont techniquement corrects mais pratiquement inutiles — le classement ne peut pas déterminer quelle note de « configuration » est pertinente pour la requête en cours.

Tokenizer FTS5

FTS5 utilise le tokenizer unicode61 par défaut, qui gère le texte ASCII et Unicode. Pour les coffres-forts contenant beaucoup de contenu CJK (chinois, japonais, coréen), envisagez le tokenizer trigram :

-- For CJK-heavy vaults
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id,
    tokenize='trigram'
);

Le tokenizer unicode61 par défaut découpe aux limites de mots, ce qui fonctionne mal pour les langues sans espaces entre les mots. Le tokenizer trigram découpe tous les trois caractères, permettant la recherche de sous-chaînes au prix d’un index plus volumineux (environ 3 fois plus grand).

Maintenance

FTS5 nécessite une synchronisation explicite lorsque la table chunks sous-jacente est modifiée :

# After inserting chunks
cursor.execute("""
    INSERT INTO chunks_fts(chunks_fts)
    VALUES('rebuild')
""")

La commande rebuild reconstruit l’index FTS5 à partir de la table de contenu. Exécutez-la après des insertions en masse (réindexation complète) mais pas après des mises à jour incrémentales individuelles — pour celles-ci, utilisez INSERT INTO chunks_fts(rowid, chunk_text, section, heading_context) pour synchroniser les lignes individuellement.


Recherche vectorielle avec sqlite-vec

L’extension sqlite-vec apporte la recherche vectorielle KNN (K-Nearest Neighbors) dans SQLite. Cette section couvre la configuration de sqlite-vec, le pipeline d’embeddings de la note au vecteur interrogeable et les schémas de requêtes spécifiques.

Table virtuelle sqlite-vec

CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

Le module vec0 stocke des vecteurs flottants à 256 dimensions sous forme de données binaires compactées. La colonne id correspond 1:1 à la table chunks, permettant des jointures entre les résultats vectoriels et les métadonnées des fragments.

Pipeline d’embeddings

Le pipeline transforme une note en vecteur interrogeable :

Note (.md file)
   Chunker: split at H2 boundaries
     Chunks (30-2000 chars each)
       Credential filter: scrub secrets
         Embedder: Model2Vec encode
           Vectors (256-dim float arrays)
             sqlite-vec: store as packed binary
               Ready for KNN queries

Sérialisation des vecteurs

Le module struct de Python sérialise les vecteurs flottants pour le stockage sqlite-vec :

import struct

def _serialize_vector(vec):
    """Pack float list into binary for sqlite-vec."""
    return struct.pack(f"{len(vec)}f", *vec)

def _deserialize_vector(blob, dim=256):
    """Unpack binary blob to float list."""
    return list(struct.unpack(f"{dim}f", blob))

Requête KNN

Une requête de recherche vectorielle encode la requête d’entrée sous forme d’embedding, puis trouve les K fragments les plus proches par distance cosinus :

def _vector_search(self, query_text, limit=30):
    query_vec = self.embedder.embed_batch([query_text])[0]
    packed = _serialize_vector(query_vec)

    results = self.db.execute("""
        SELECT
            cv.id,
            cv.distance,
            c.file_path,
            c.section,
            c.chunk_text
        FROM chunk_vecs cv
        JOIN chunks c ON cv.id = c.id
        WHERE embedding MATCH ?
            AND k = ?
        ORDER BY distance
    """, [packed, limit]).fetchall()

    return results

L’opérateur MATCH dans sqlite-vec effectue une recherche approximative du plus proche voisin. Le paramètre k contrôle le nombre de résultats à renvoyer. La colonne distance contient la distance cosinus (0 = identique, 2 = opposé).

Quand la recherche vectorielle l’emporte

La recherche vectorielle excelle pour les requêtes où le concept importe plus que les mots spécifiques :

  • Requête : « how to handle authentication failures » → Trouve des notes sur « login error recovery » (même espace sémantique, mots-clés différents)
  • Requête : « what patterns exist for caching » → Trouve des notes sur « memoization », « Redis TTL strategies » et « HTTP cache headers » (concepts liés, terminologie diverse)
  • Requête : « approaches to testing asynchronous code » → Trouve des notes sur « pytest-asyncio fixtures », « mock event loops » et « async test patterns » (même concept exprimé à travers des détails d’implémentation)

Quand la recherche vectorielle échoue

La recherche vectorielle a du mal avec les identifiants exacts :

  • Requête : _rrf_fuse → Renvoie des notes sur les « algorithmes de fusion » et le « rank merging » mais peut classer la définition réelle de la fonction plus bas que les discussions conceptuelles
  • Requête : PostToolUse → Renvoie des notes sur les « hooks de cycle de vie des outils » et les « gestionnaires post-exécution » plutôt que le nom spécifique du hook

La recherche vectorielle a également du mal avec les données structurées. Les fichiers de configuration JSON, les blocs YAML et les extraits de code produisent des embeddings qui capturent des schémas structurels plutôt que du sens sémantique. Un fichier JSON avec "review": true génère un embedding différent d’une discussion en prose sur la revue de code.

Dégradation gracieuse

Si sqlite-vec ne parvient pas à se charger (extension manquante, plateforme incompatible, bibliothèque corrompue), le système de récupération se rabat sur une recherche BM25 uniquement :

class VectorIndex:
    def __init__(self, db_path):
        self.db = sqlite3.connect(db_path)
        self._vec_available = False
        try:
            self.db.enable_load_extension(True)
            self.db.load_extension("vec0")
            self._vec_available = True
        except Exception:
            pass  # BM25-only mode

    @property
    def vec_available(self):
        return self._vec_available

Le système de récupération vérifie vec_available avant de tenter des requêtes vectorielles. Lorsque cette option est désactivée, toutes les recherches utilisent uniquement BM25 et l’étape de fusion RRF est ignorée.


Reciprocal Rank Fusion (RRF)

RRF fusionne deux listes classées sans nécessiter de calibration des scores. Cette section couvre l’algorithme, un exemple détaillé de requête, le réglage du paramètre k, et les raisons pour lesquelles RRF est préféré aux alternatives. Pour un calculateur interactif avec des rangs modifiables, des scénarios prédéfinis et un explorateur visuel de l’architecture, consultez l’analyse approfondie du retriever hybride.

L’algorithme

RRF attribue à chaque document un score basé uniquement sur sa position dans chaque liste :

score(d) = Σ (weight_i / (k + rank_i))

Où : - k est une constante de lissage (60, selon Cormack et al.1) - rank_i est le rang du document (indexé à partir de 1) dans la liste de résultats i - weight_i est un multiplicateur optionnel par liste (par défaut 1.0)

Les documents bien classés dans plusieurs listes obtiennent des scores fusionnés plus élevés. Les documents qui n’apparaissent que dans une seule liste reçoivent un score provenant de cette unique source.

Pourquoi RRF plutôt que les alternatives

La combinaison linéaire pondérée nécessite de calibrer les scores BM25 par rapport aux distances cosinus. Les scores BM25 ne sont pas bornés et varient selon la taille du corpus. Les distances cosinus sont bornées [0, 2]. Les combiner exige une normalisation, et les paramètres de normalisation dépendent du jeu de données. RRF utilise uniquement les positions de rang, qui sont toujours des entiers commençant à 1, indépendamment de la méthode de scoring.

Les modèles de fusion appris nécessitent des données d’entraînement étiquetées — des paires de pertinence requête-document. Pour une base de connaissances personnelle, ces données d’entraînement n’existent pas. Il faudrait évaluer manuellement des centaines de paires requête-document pour entraîner un modèle utile. RRF fonctionne sans aucune donnée d’entraînement.

Les méthodes de vote Condorcet (comptage de Borda, méthode de Schulze) sont théoriquement élégantes mais plus complexes à implémenter et à régler. L’article original sur RRF a démontré que RRF surpasse les méthodes Condorcet sur les données d’évaluation TREC.1

La fusion en pratique

Requête : « how does the review aggregator handle disagreements »

BM25 classe review-aggregator.py en position 3 (correspondances exactes de mots-clés sur « review », « aggregator », « disagreements ») mais place deux fichiers de configuration plus haut (ils correspondent davantage à « review »). La recherche vectorielle classe le même chunk en position 1 (correspondance sémantique sur la résolution de conflits). Après la fusion RRF :

Chunk BM25 Vec Score fusionné
review-aggregator.py “Disagreement Resolution” #3 #1 0.0323
code-review-patterns.md “Multi-Reviewer” #4 #2 0.0317
deliberation-config.json “Review Weights” #1 0.0164

Les chunks bien classés dans les deux listes remontent en tête. Les chunks qui n’apparaissent que dans une seule liste obtiennent un score provenant d’une source unique et se retrouvent en dessous des résultats classés dans les deux listes. La logique de résolution des désaccords l’emporte parce que les deux méthodes l’ont trouvée — BM25 grâce aux mots-clés, la recherche vectorielle grâce à la sémantique.

Pour le détail complet étape par étape avec le calcul RRF par rang, essayez différentes valeurs de k dans le calculateur interactif RRF.

Implémentation

RRF_K = 60

def _rrf_fuse(self, bm25_results, vec_results,
              bm25_weight=1.0, vec_weight=1.0):
    """Fuse BM25 and vector results using Reciprocal Rank Fusion."""
    scores = {}

    for rank, r in enumerate(bm25_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += bm25_weight / (self._rrf_k + rank)
        scores[cid]["bm25_rank"] = rank

    for rank, r in enumerate(vec_results, start=1):
        cid = r["id"]
        if cid not in scores:
            scores[cid] = {
                "rrf_score": 0.0,
                "file_path": r["file_path"],
                "section": r["section"],
                "chunk_text": r["chunk_text"],
                "bm25_rank": None,
                "vec_rank": None,
            }
        scores[cid]["rrf_score"] += vec_weight / (self._rrf_k + rank)
        scores[cid]["vec_rank"] = rank

    fused = sorted(
        scores.values(),
        key=lambda x: x["rrf_score"],
        reverse=True,
    )
    return fused

Réglage de k

La constante k contrôle le poids accordé aux résultats les mieux classés par rapport aux résultats moins bien classés :

  • k faible (ex. 10) : Les résultats les mieux classés dominent. Le rang 1 obtient 1/11 = 0,091, le rang 10 obtient 1/20 = 0,050 (différence de 1,8x). Adapté lorsque vous faites confiance aux classements individuels pour identifier correctement le meilleur résultat.
  • k par défaut (60) : Équilibré. Le rang 1 obtient 1/61 = 0,0164, le rang 10 obtient 1/70 = 0,0143 (différence de 1,15x). Les écarts de rang sont compressés, ce qui accorde plus de poids au fait d’apparaître dans plusieurs listes.
  • k élevé (ex. 200) : Apparaître dans les deux listes compte bien plus que la position de rang. Le rang 1 obtient 1/201, le rang 10 obtient 1/210 — quasi identiques. À utiliser lorsque les classements individuels sont bruités mais que l’accord entre les listes est fiable.

Commencez avec k=60. L’article original sur RRF a démontré la robustesse de cette valeur sur des jeux de données TREC variés. Ne modifiez ce paramètre qu’après avoir mesuré les cas d’échec sur votre propre distribution de requêtes.

Résolution des égalités

Lorsque deux chunks ont des scores RRF identiques (rare mais possible avec le même rang dans une liste et aucune apparition dans l’autre), résolvez les égalités selon l’ordre suivant :

  1. Préférez les chunks qui apparaissent dans les deux listes à ceux qui n’apparaissent que dans une seule
  2. Parmi les chunks présents dans les deux listes, préférez celui dont le rang combiné est le plus bas
  3. Parmi les chunks présents dans une seule liste, préférez celui dont le rang est le plus bas dans cette liste

Le pipeline de recherche complet

Cette section retrace une requête de l’entrée à la sortie à travers l’ensemble du pipeline : recherche BM25, recherche vectorielle, fusion RRF, troncature par budget de tokens et assemblage du contexte.

Flux de bout en bout

User query: "PostToolUse hook for context compression"
  │
  ├─ BM25 Search (FTS5)
  │    → MATCH "PostToolUse hook context compression"
  │    → Top 30 results ranked by BM25 score
  │    → 12ms
  │
  ├─ Vector Search (sqlite-vec)
  │    → Embed query with Model2Vec
  │    → KNN k=30 on chunk_vecs
  │    → Top 30 results ranked by cosine distance
  │    → 8ms
  │
  └─ RRF Fusion
       → Merge 60 candidates (may overlap)
       → Score by rank position
       → Top 10 results
       → 3ms
       │
       └─ Token Budget
            → Truncate to max_tokens (default 4000)
            → Estimate at 4 chars per token
            → Return results with metadata
            → <1ms

Latence totale : ~23 ms pour une base de données de 49 746 chunks sur du matériel Apple M3 Pro.

La API de recherche

class HybridRetriever:
    def search(self, query, limit=10, max_tokens=4000,
               bm25_weight=1.0, vec_weight=1.0):
        """
        Search the vault using hybrid BM25 + vector retrieval.

        Args:
            query: Search query text
            limit: Maximum results to return
            max_tokens: Token budget for total result text
            bm25_weight: Weight for BM25 results in RRF
            vec_weight: Weight for vector results in RRF

        Returns:
            List of SearchResult with file_path, section,
            chunk_text, rrf_score, bm25_rank, vec_rank
        """
        # BM25 search
        bm25_results = self._bm25_search(query, limit=30)

        # Vector search (if available)
        if self.index.vec_available:
            vec_results = self._vector_search(query, limit=30)
            fused = self._rrf_fuse(
                bm25_results, vec_results,
                bm25_weight, vec_weight,
            )
        else:
            fused = bm25_results  # BM25-only fallback

        # Token budget truncation
        results = []
        token_count = 0
        for r in fused[:limit]:
            chunk_tokens = len(r["chunk_text"]) // 4
            if token_count + chunk_tokens > max_tokens:
                break
            results.append(r)
            token_count += chunk_tokens

        return results

Troncature par budget de tokens

Le paramètre max_tokens empêche le système de recherche de renvoyer plus de contexte que ce que l’outil IA peut exploiter. L’estimation utilise 4 caractères par token (une approximation raisonnable pour la prose anglaise). Les résultats sont tronqués de manière gloutonne : on ajoute les résultats par ordre de classement jusqu’à épuisement du budget.

Il s’agit d’une stratégie conservatrice. Une approche plus sophistiquée prendrait en compte les scores de qualité par résultat et privilégierait les résultats plus courts et de meilleure qualité par rapport aux résultats plus longs et de moindre qualité. L’approche gloutonne est plus simple et fonctionne bien en pratique, car le classement RRF ordonne déjà les résultats par pertinence.

Schéma de la base de données (complet)

-- Chunk content and metadata
CREATE TABLE chunks (
    id INTEGER PRIMARY KEY,
    file_path TEXT NOT NULL,
    section TEXT NOT NULL,
    chunk_text TEXT NOT NULL,
    heading_context TEXT DEFAULT '',
    mtime_ns INTEGER NOT NULL,
    embedded_at REAL NOT NULL
);

CREATE INDEX idx_chunks_file ON chunks(file_path);
CREATE INDEX idx_chunks_mtime ON chunks(mtime_ns);

-- FTS5 for BM25 search (content-synced to chunks table)
CREATE VIRTUAL TABLE chunks_fts USING fts5(
    chunk_text, section, heading_context,
    content=chunks, content_rowid=id
);

-- sqlite-vec for vector KNN search
CREATE VIRTUAL TABLE chunk_vecs USING vec0(
    id INTEGER PRIMARY KEY,
    embedding float[256]
);

-- Model metadata for compatibility tracking
CREATE TABLE model_meta (
    key TEXT PRIMARY KEY,
    value TEXT
);

Chemin de dégradation progressive

Full pipeline:     BM25 + Vector + RRF    Best results
No sqlite-vec:     BM25 only              Good results (no semantic)
No model download:  BM25 only              Good results (no semantic)
No FTS5:           Vector only             Decent results (no keyword)
No database:       Error                   Prompt user to run indexer

Le système de recherche vérifie les capacités disponibles à l’initialisation et adapte sa stratégie de requête en conséquence. Un composant manquant dégrade la qualité mais ne provoque pas d’erreur. Le seul échec critique est l’absence du fichier de base de données.

Statistiques en production

Mesures effectuées sur un coffre-fort de 16 894 fichiers, 49 746 chunks, base de données SQLite de 83 Mo, Apple M3 Pro :

Métrique Valeur
Total de fichiers 16 894
Total de chunks 49 746
Taille de la base de données 83 Mo
Latence de requête BM25 (p50) 12 ms
Latence de requête vectorielle (p50) 8 ms
Latence de fusion RRF 3 ms
Latence de recherche de bout en bout (p50) 23 ms
Temps de réindexation complète ~4 minutes
Temps de réindexation incrémentale <10 secondes
Modèle d’embeddings potion-base-8M (256-dim)
Pool de candidats BM25 30
Pool de candidats vectoriels 30
Limite de résultats par défaut 10
Budget de tokens par défaut 4 000 tokens

Hachage du contenu et détection des modifications

L’indexeur doit savoir quels fichiers ont changé depuis la dernière exécution de l’indexation. Cette section couvre le mécanisme de détection des modifications et la stratégie de hachage.

Comparaison des dates de modification des fichiers

L’indexeur stocke mtime_ns (date de modification du fichier en nanosecondes) pour chaque chunk dans la table chunks. Lors d’une exécution incrémentale, l’indexeur :

  1. Parcourt le coffre-fort à la recherche de tous les fichiers .md dans les dossiers autorisés
  2. Lit le mtime_ns de chaque fichier depuis le système de fichiers
  3. Compare avec le mtime_ns stocké dans la base de données
  4. Identifie trois catégories :
  5. Nouveaux fichiers : le chemin existe dans le système de fichiers mais pas dans la base de données
  6. Fichiers modifiés : le chemin existe dans les deux mais le mtime_ns diffère
  7. Fichiers supprimés : le chemin existe dans la base de données mais pas dans le système de fichiers
def get_stale_files(self, vault_mtimes):
    """Find files whose mtime changed or are new."""
    stored = dict(self.db.execute(
        "SELECT DISTINCT file_path, mtime_ns FROM chunks"
    ).fetchall())

    stale = []
    for path, mtime in vault_mtimes.items():
        if path not in stored or stored[path] != mtime:
            stale.append(path)
    return stale

def get_deleted_files(self, vault_paths):
    """Find files in database that no longer exist in vault."""
    stored_paths = set(r[0] for r in self.db.execute(
        "SELECT DISTINCT file_path FROM chunks"
    ).fetchall())
    return stored_paths - set(vault_paths)

Pourquoi mtime plutôt qu’un hash du contenu

Le hachage du contenu (SHA-256 du contenu du fichier) serait plus fiable que la comparaison de mtime — il détecterait les cas où un fichier a été touché sans être modifié (par exemple, un git checkout restaurant le mtime d’origine). Cependant, le hachage nécessite de lire chaque fichier à chaque exécution incrémentale. Pour 16 894 fichiers, la lecture du contenu prend 2 à 3 secondes. La lecture des mtimes depuis le système de fichiers prend moins de 100 ms.

Le compromis : la comparaison de mtime déclenche parfois une réindexation inutile de fichiers inchangés (faux positifs), mais ne manque jamais les modifications réelles. Les faux positifs coûtent quelques appels d’embeddings supplémentaires par exécution. La différence de vitesse (100 ms contre 3 secondes) fait de mtime le choix pragmatique pour un système qui s’exécute à chaque interaction avec l’IA.

Gestion des suppressions

Lorsqu’un fichier est supprimé du coffre-fort, l’indexeur retire tous ses chunks de la base de données :

def remove_file(self, file_path):
    """Remove all chunks and vectors for a file."""
    chunk_ids = [r[0] for r in self.db.execute(
        "SELECT id FROM chunks WHERE file_path = ?",
        [file_path],
    ).fetchall()]

    for cid in chunk_ids:
        self.db.execute(
            "DELETE FROM chunk_vecs WHERE id = ?", [cid]
        )
    self.db.execute(
        "DELETE FROM chunks WHERE file_path = ?",
        [file_path],
    )

Les tables FTS5 synchronisées par contenu nécessitent une suppression explicite via INSERT INTO chunks_fts(chunks_fts, rowid, ...) VALUES('delete', ?, ...) pour chaque ligne retirée. L’indexeur gère cette opération dans le cadre du processus de suppression de fichier.


Réindexation incrémentale vs complète

L’indexeur prend en charge deux modes : incrémental (rapide, usage quotidien) et complet (lent, occasionnel). Cette section explique quand utiliser chacun, les garanties d’idempotence et la récupération après corruption.

Réindexation incrémentale

Quand l’utiliser : indexation quotidienne après la modification de notes. C’est le mode par défaut.

Ce qu’il fait : 1. Analyse le coffre-fort à la recherche de fichiers modifiés (comparaison des mtime) 2. Supprime les fragments des fichiers supprimés 3. Redécoupe et recalcule les embeddings des fichiers modifiés 4. Insère de nouveaux fragments pour les nouveaux fichiers 5. Synchronise l’index FTS5

Durée typique : moins de 10 secondes pour les modifications d’une journée sur un coffre-fort de 16 000 fichiers.

python index_vault.py --incremental

Réindexation complète

Quand l’utiliser : - Après un changement de modèle d’embedding (détection d’une divergence du hash du modèle) - Après une migration de schéma (nouvelles colonnes, index modifiés) - Après une corruption de la base de données (l’intégrité échoue à la vérification) - Lorsque l’indexation incrémentale produit des résultats inattendus

Ce qu’il fait : 1. Supprime toutes les données existantes (fragments, vecteurs, entrées FTS5) 2. Analyse l’intégralité du coffre-fort 3. Découpe tous les fichiers en fragments 4. Calcule les embeddings de tous les fragments 5. Reconstruit l’index FTS5 depuis zéro

Durée typique : environ 4 minutes pour 16 894 fichiers sur Apple M3 Pro.

python index_vault.py --full

Idempotence

Les deux modes sont idempotents : exécuter la même commande deux fois produit le même résultat. L’indexeur supprime les fragments existants d’un fichier avant d’en insérer de nouveaux, de sorte qu’une réexécution de l’indexation incrémentale sur une base de données déjà à jour ne produit aucun changement. Une réexécution de l’indexation complète produit une base de données identique.

Récupération après corruption

Si la base de données SQLite est corrompue (coupure de courant pendant une écriture, erreur disque, processus interrompu en cours de transaction) :

# Check integrity
sqlite3 vectors.db "PRAGMA integrity_check;"

# If corruption detected, full reindex rebuilds from source files
python index_vault.py --full

La source de vérité est toujours constituée par les fichiers du coffre-fort, pas par la base de données. La base de données est un artefact dérivé qui peut être reconstruit à tout moment. C’est une propriété de conception essentielle : vous n’avez jamais besoin de sauvegarder la base de données.

L’option --incremental

Lorsque l’indexeur s’exécute avec --incremental :

  1. Vérification du hash du modèle. Compare le hash du modèle stocké avec le modèle actuel. En cas de divergence, bascule automatiquement en mode de réindexation complète et avertit l’utilisateur.
  2. Analyse des fichiers. Parcourt les dossiers autorisés, collecte les chemins de fichiers et les mtimes.
  3. Détection des changements. Compare avec les données stockées.
  4. Traitement par lots. Redécoupe et recalcule les embeddings des fichiers modifiés par lots de 64.
  5. Rapport de progression. Affiche le nombre de fichiers traités et le temps écoulé.
  6. Arrêt gracieux. Gère le signal SIGINT en terminant le fichier en cours avant de s’arrêter.

Filtrage des identifiants et limites des données

Les notes personnelles contiennent des secrets : clés API, jetons bearer, chaînes de connexion aux bases de données, clés privées collées lors de sessions de débogage. Le filtre d’identifiants empêche ces éléments d’entrer dans l’index de recherche.

Le problème

Une note sur le débogage d’une intégration OAuth pourrait contenir :

The token was: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
I used this curl command:
  curl -H "Authorization: Bearer sk-ant-api03-abc123..."

Sans filtrage, le JWT et la clé API seraient découpés en fragments, transformés en embeddings et stockés dans la base de données. Une recherche sur « authentification » renverrait le fragment contenant de véritables secrets. Pire encore, si le système de recherche transmet les résultats à un outil d’IA via MCP, les secrets apparaissent dans la fenêtre de contexte de l’IA et potentiellement dans les journaux de l’outil.

Filtrage par motifs

Le filtre d’identifiants s’exécute sur chaque fragment avant stockage, en utilisant 25 motifs spécifiques aux fournisseurs ainsi que des motifs génériques :

Motifs spécifiques aux fournisseurs :

Motif Exemple Regex
Clé API OpenAI sk-... sk-[a-zA-Z0-9_-]{20,}
Clé API Anthropic sk-ant-api03-... sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,}
PAT GitHub ghp_... gh[ps]_[a-zA-Z0-9]{36,}
Clé d’accès AWS AKIA... AKIA[0-9A-Z]{16}
Clé Stripe sk_live_... [sr]k_(live\|test)_[a-zA-Z0-9]{24,}
Jeton Cloudflare ... Divers motifs

Motifs génériques :

Motif Détection
Jetons JWT eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+
Jetons Bearer Bearer\s+[a-zA-Z0-9_\-\.]+
Clés privées -----BEGIN (RSA\|EC\|OPENSSH) PRIVATE KEY-----
Base64 à haute entropie Chaînes avec >4,5 bits/caractère d’entropie, 40+ caractères
Assignations de mots de passe password\s*[:=]\s*["'][^"']+["']

Implémentation du filtre

def clean_content(text):
    """Scrub credentials from text before indexing."""
    result = ScanResult(is_clean=True, match_count=0, patterns=[])

    for pattern in CREDENTIAL_PATTERNS:
        matches = pattern.regex.findall(text)
        if matches:
            text = pattern.regex.sub(
                f"[REDACTED:{pattern.name}]", text
            )
            result.is_clean = False
            result.match_count += len(matches)
            result.patterns.append(pattern.name)

    return text, result

Choix de conception clés :

  1. Filtrer avant le calcul des embeddings. C’est le texte nettoyé qui est transformé en embedding. La représentation vectorielle n’encode jamais les motifs d’identifiants. Une requête pour « clé API » renvoie les notes qui traitent de la gestion des clés API, pas les notes qui contiennent de véritables clés.

  2. Remplacer, pas supprimer. Le jeton [REDACTED:pattern-name] préserve le contexte sémantique du texte environnant. L’embedding capture le fait que « quelque chose ressemblant à un identifiant se trouvait ici » sans encoder l’identifiant lui-même.

  3. Journaliser les motifs, pas les valeurs. Le filtre enregistre quels motifs ont été détectés (par exemple, « Scrubbed 2 credential(s) from oauth-debug.md [jwt, bearer-token] ») mais ne journalise jamais la valeur de l’identifiant.

Exclusion par chemin

Le fichier .indexignore offre une exclusion à gros grain par chemin. Le filtre d’identifiants offre un nettoyage à grain fin au sein des fichiers indexés. Les deux sont nécessaires :

  • .indexignore pour les dossiers entiers dont vous savez qu’ils contiennent du contenu sensible (notes de santé, documents financiers, documents de carrière)
  • Le filtre d’identifiants pour les secrets accidentellement intégrés dans du contenu par ailleurs indexable

Classification des données

Pour les coffres-forts contenant du contenu diversifié, envisagez de classer les notes par niveau de sensibilité :

Niveau Exemples Indexer ? Filtrer ?
Public Brouillons de blog, notes techniques Oui Oui
Interne Plans de projet, décisions d’architecture Oui Oui
Sensible Données salariales, dossiers médicaux Non (.indexignore) N/A
Restreint Identifiants, clés privées Non (.indexignore) N/A

Architecture du serveur MCP

Le protocole Model Context Protocol (MCP) permet d’exposer le système de recherche sous forme d’outil que les agents IA peuvent appeler. Cette section couvre la conception du serveur, la surface de fonctionnalités et les limites de permissions.

Choix du protocole : STDIO vs HTTP

MCP prend en charge deux modes de transport :

STDIO — L’outil IA lance le serveur MCP en tant que processus enfant et communique via stdin/stdout. C’est le mode standard pour les outils locaux. Claude Code, Codex CLI et Cursor prennent tous en charge les serveurs MCP en mode STDIO.

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/path/to/vault",
        "DB_PATH": "/path/to/vectors.db"
      }
    }
  }
}

HTTP — Le serveur MCP fonctionne comme un service HTTP autonome. Utile pour l’accès distant, les configurations multi-clients ou les configurations d’équipe où le coffre se trouve sur un serveur partagé.

{
  "mcpServers": {
    "obsidian": {
      "url": "http://localhost:3333/mcp"
    }
  }
}

Recommandation : Utilisez STDIO pour les coffres personnels. C’est plus simple, plus sécurisé (aucune exposition réseau) et le cycle de vie du serveur est géré par l’outil IA. N’utilisez HTTP que lorsque plusieurs outils ou plusieurs machines nécessitent un accès concurrent au même coffre.

Évolution de la spécification MCP. La spécification MCP de juin 2025 a ajouté l’autorisation OAuth 2.1, les sorties d’outils structurées (schémas de retour typés) et l’élicitation (invites utilisateur initiées par le serveur). La version de novembre 2025 a introduit le transport Streamable HTTP en tant que mode de transport de première classe, la découverte d’URL .well-known pour la navigation automatique des capacités du serveur, les annotations d’outils structurées qui déclarent si un outil est en lecture seule ou mutatif, et un système de standardisation des niveaux SDK.1619 La prochaine version de la spécification (provisoirement mi-2026) propose des opérations asynchrones pour les tâches de longue durée, des extensions de protocole spécifiques à certains domaines pour des secteurs comme la santé et la finance, ainsi que des standards de communication agent-à-agent pour les workflows multi-agents.19 Pour les serveurs de coffre personnels, STDIO reste le chemin le plus simple. Le transport Streamable HTTP et la découverte .well-known bénéficient principalement aux déploiements HTTP en entreprise avec routage multi-tenant et répartition de charge. Suivez la feuille de route MCP pour les mises à jour qui affectent votre choix de transport.

Conception des fonctionnalités

Le serveur MCP devrait exposer un ensemble minimal d’outils :

search — L’outil principal. Exécute une recherche hybride et renvoie des résultats classés.

{
  "name": "obsidian_search",
  "description": "Search the Obsidian vault using hybrid BM25 + vector retrieval",
  "parameters": {
    "query": { "type": "string", "description": "Search query" },
    "limit": { "type": "integer", "default": 5 },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

read_note — Lit le contenu complet d’une note spécifique par son chemin. Utile lorsque l’agent souhaite voir le contexte complet d’un résultat de recherche.

{
  "name": "obsidian_read_note",
  "description": "Read the full content of a note by file path",
  "parameters": {
    "file_path": { "type": "string", "description": "Relative path within vault" }
  }
}

list_notes — Liste les notes correspondant à un filtre (par dossier, tag, type ou plage de dates). Utile pour l’exploration lorsque l’agent n’a pas de requête spécifique.

{
  "name": "obsidian_list_notes",
  "description": "List notes matching filters",
  "parameters": {
    "folder": { "type": "string", "description": "Folder path within vault" },
    "tag": { "type": "string", "description": "Tag to filter by" },
    "limit": { "type": "integer", "default": 20 }
  }
}

get_context — Un outil pratique qui exécute une recherche et formate les résultats sous forme de bloc de contexte adapté à l’injection dans une conversation.

{
  "name": "obsidian_get_context",
  "description": "Get formatted context from vault for a topic",
  "parameters": {
    "topic": { "type": "string", "description": "Topic to get context for" },
    "max_tokens": { "type": "integer", "default": 2000 }
  }
}

Limites de permissions

Le serveur MCP doit appliquer des limites strictes :

  1. Lecture seule. Le serveur lit le coffre et la base de données d’index. Il ne crée, ne modifie ni ne supprime de notes. Les opérations d’écriture (capture de nouvelles notes) sont gérées par des hooks ou des skills séparés, et non par le serveur MCP.

  2. Limité au coffre. Le serveur ne lit que les fichiers situés dans le chemin du coffre configuré. Les tentatives de traversée de chemin (../../etc/passwd) doivent être rejetées.

  3. Filtrage des identifiants en sortie. Même si la base de données contient du contenu pré-filtré, appliquez le filtrage des identifiants en sortie comme mesure de défense en profondeur.

  4. Réponses limitées en tokens. Appliquez max_tokens sur toutes les réponses d’outils pour empêcher l’outil IA de recevoir des blocs de contexte excessivement volumineux.

Gestion des erreurs

Les outils MCP devraient renvoyer des messages d’erreur structurés qui aident l’outil IA à récupérer :

def search(self, query, limit=5, max_tokens=2000):
    if not self.db_path.exists():
        return {
            "error": "Index database not found. Run the indexer first.",
            "suggestion": "python index_vault.py --full"
        }

    results = self.retriever.search(query, limit, max_tokens)

    if not results:
        return {
            "results": [],
            "message": f"No results found for '{query}'. Try broader terms."
        }

    return {
        "results": [
            {
                "file_path": r["file_path"],
                "section": r["section"],
                "text": r["chunk_text"],
                "score": round(r["rrf_score"], 4),
            }
            for r in results
        ],
        "count": len(results),
        "query": query,
    }

Intégration avec Claude Code

Claude Code est le principal consommateur du système de recherche Obsidian. Cette section couvre la configuration MCP, l’intégration des hooks et le pattern obsidian_bridge.py.

Configuration MCP

Ajoutez le serveur MCP Obsidian à ~/.claude/settings.json :

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

Après avoir ajouté la configuration, redémarrez Claude Code. Le serveur MCP démarrera en tant que processus enfant. Vérifiez qu’il fonctionne :

> What tools do you have from the obsidian MCP server?

Claude Code devrait lister les outils disponibles (obsidian_search, obsidian_read_note, etc.).

Intégration des hooks

Les hooks étendent le comportement de Claude Code à des points définis du cycle de vie. Deux hooks sont pertinents pour l’intégration avec Obsidian :

Hook PreToolUse — Interroge le coffre avant que l’agent ne traite un appel d’outil. Injecte automatiquement le contexte pertinent.

#!/bin/bash
# ~/.claude/hooks/pre-tool-use/obsidian-context.sh
# Automatically inject vault context before tool execution

TOOL_NAME="$1"
PROMPT="$2"

# Only inject context for code-related tools
case "$TOOL_NAME" in
    Edit|Write|Bash)
        # Query the vault
        CONTEXT=$(python /path/to/retriever.py search "$PROMPT" --limit 3 --max-tokens 1500)
        if [ -n "$CONTEXT" ]; then
            echo "---"
            echo "Relevant vault context:"
            echo "$CONTEXT"
            echo "---"
        fi
        ;;
esac

Hook PostToolUse — Capture les sorties significatives des outils dans le coffre pour une récupération ultérieure.

#!/bin/bash
# ~/.claude/hooks/post-tool-use/capture-insight.sh
# Capture significant outputs to vault (selective)

TOOL_NAME="$1"
OUTPUT="$2"

# Only capture substantial outputs
if [ ${#OUTPUT} -gt 500 ]; then
    python /path/to/capture.py --text "$OUTPUT" --source "claude-code-$TOOL_NAME"
fi

Le pattern obsidian_bridge.py

Un module de pont fournit une Python API que les hooks et les skills peuvent appeler :

# obsidian_bridge.py
from retriever import HybridRetriever

_retriever = None

def get_retriever():
    global _retriever
    if _retriever is None:
        _retriever = HybridRetriever(
            db_path="/path/to/vectors.db",
            vault_path="/path/to/vault",
        )
    return _retriever

def search_vault(query, limit=5, max_tokens=2000):
    """Search vault and return formatted context."""
    retriever = get_retriever()
    results = retriever.search(query, limit, max_tokens)

    if not results:
        return ""

    lines = ["## Vault Context\n"]
    for r in results:
        lines.append(f"**{r['file_path']}** — {r['section']}")
        lines.append(f"> {r['chunk_text'][:500]}")
        lines.append("")

    return "\n".join(lines)

Le skill /capture

Un skill Claude Code pour capturer des informations dans le coffre :

/capture "OAuth token rotation requires both access and refresh token invalidation"
  --domain security
  --tags oauth,tokens

Le skill crée une nouvelle note dans 00-inbox/ avec les métadonnées frontmatter appropriées et déclenche une réindexation incrémentale pour que la nouvelle note soit immédiatement consultable.

Gestion de la fenêtre de contexte

L’intégration doit être attentive à la fenêtre de contexte de Claude Code :

  • Limitez le contexte injecté à 1 500-2 000 tokens par requête. Au-delà, cela entre en concurrence avec la mémoire de travail de l’agent.
  • Incluez l’attribution de la source. Incluez toujours le chemin du fichier et le titre de la section pour que l’agent puisse référencer la source.
  • Tronquez le texte des fragments. Les fragments longs devraient être tronqués avec ... plutôt qu’omis entièrement. Les 300 à 500 premiers caractères contiennent généralement les informations clés.
  • N’injectez pas à chaque appel d’outil. Le hook PreToolUse devrait injecter le contexte de manière sélective en fonction de l’outil appelé. Les opérations de lecture n’ont pas besoin du contexte du coffre. Les opérations d’écriture et de modification en bénéficient.

Intégration de Codex CLI

Codex CLI se connecte aux serveurs MCP via config.toml. Le modèle d’intégration diffère de Claude Code au niveau de la syntaxe de configuration et de la transmission des instructions.

Configuration MCP

Ajoutez dans .codex/config.toml ou ~/.codex/config.toml :

[mcp_servers.obsidian]
command = "python"
args = ["/path/to/obsidian_mcp.py"]

[mcp_servers.obsidian.env]
VAULT_PATH = "/absolute/path/to/vault"
DB_PATH = "/absolute/path/to/vectors.db"

Modèles AGENTS.md

Codex CLI lit AGENTS.md pour les instructions au niveau du projet. Incluez des directives de recherche dans le coffre :

## Available Tools

### Obsidian Vault (MCP: obsidian)
Use the `obsidian_search` tool to find relevant context from the knowledge base.
Search the vault when you need:
- Background on a concept or pattern
- Prior decisions or rationale
- Reference material for implementation

Example queries:
- "authentication patterns in FastAPI"
- "how does the review aggregator work"
- "sqlite-vec configuration"

Différences avec Claude Code

Fonctionnalité Claude Code Codex CLI
Configuration MCP settings.json config.toml
Hooks ~/.claude/hooks/ Non pris en charge
Skills ~/.claude/skills/ Non pris en charge
Fichier d’instructions CLAUDE.md AGENTS.md
Modes d’approbation --dangerously-skip-permissions suggest / auto-edit / full-auto

Différence clé : Codex CLI ne prend pas en charge les hooks. Le modèle d’injection automatique de contexte (hook PreToolUse) n’est pas disponible. À la place, incluez des instructions explicites dans AGENTS.md indiquant à l’agent de rechercher dans le coffre avant de commencer le travail.


Cursor et autres outils

Cursor et les autres outils d’IA prenant en charge MCP peuvent se connecter au même serveur MCP Obsidian. Cette section couvre la configuration des outils courants.

Cursor

Ajoutez dans .cursor/mcp.json à la racine de votre projet :

{
  "mcpServers": {
    "obsidian": {
      "command": "python",
      "args": ["/path/to/obsidian_mcp.py"],
      "env": {
        "VAULT_PATH": "/absolute/path/to/vault",
        "DB_PATH": "/absolute/path/to/vectors.db"
      }
    }
  }
}

Le fichier .cursorrules de Cursor peut inclure des instructions pour utiliser le coffre :

When working on implementation tasks, search the Obsidian vault
for relevant context before writing code. Use the obsidian_search
tool with descriptive queries about the concept you're implementing.

Matrice de compatibilité

Outil Support MCP Transport Emplacement de configuration
Claude Code Complet STDIO ~/.claude/settings.json
Codex CLI Complet STDIO .codex/config.toml
Cursor Complet STDIO .cursor/mcp.json
Windsurf Complet STDIO .windsurf/mcp.json
Continue.dev Partiel HTTP ~/.continue/config.json
Zed En cours STDIO Interface des paramètres

Solution de repli pour les outils sans MCP

Pour les outils qui ne prennent pas en charge MCP, le système de récupération peut être encapsulé en tant que CLI :

# Search from command line
python retriever_cli.py search "query text" --limit 5

# Output formatted for copy-paste into any tool
python retriever_cli.py context "query text" --format markdown

Le CLI produit du texte structuré qui peut être copié-collé manuellement dans l’entrée de n’importe quel outil d’IA. C’est moins élégant qu’une intégration MCP, mais cela fonctionne universellement.


Mise en cache de prompts à partir de notes structurées

Les notes structurées dans le coffre peuvent servir de blocs de contexte réutilisables qui réduisent la consommation de tokens à travers les interactions avec l’IA. Cette section couvre la conception des clés de cache et la gestion du budget de tokens.

Le principe

Au lieu de rechercher du contexte à chaque interaction, pré-construisez des blocs de contexte à partir de notes de coffre bien structurées et mettez-les en cache :

# cache_keys.py
CONTEXT_BLOCKS = {
    "auth-patterns": {
        "vault_query": "authentication patterns implementation",
        "max_tokens": 1500,
        "ttl_hours": 24,  # Rebuild daily
    },
    "api-conventions": {
        "vault_query": "API design conventions REST patterns",
        "max_tokens": 1000,
        "ttl_hours": 168,  # Rebuild weekly
    },
    "project-architecture": {
        "vault_query": "current project architecture decisions",
        "max_tokens": 2000,
        "ttl_hours": 12,  # Rebuild twice daily
    },
}

Invalidation du cache

L’invalidation du cache repose sur deux signaux :

  1. Expiration du TTL. Chaque bloc de contexte possède une durée de vie (TTL). Lorsque le TTL expire, le bloc est reconstruit en interrogeant à nouveau le coffre.
  2. Détection des modifications du coffre. Lorsque l’indexeur détecte des modifications dans les fichiers qui ont contribué à un bloc de contexte mis en cache, le bloc est invalidé immédiatement.

Gestion du budget de tokens

Une session démarre avec un budget total de contexte. Les blocs mis en cache consomment une partie de ce budget :

Total context budget:    8,000 tokens
├─ System prompt:        1,500 tokens
├─ Cached blocks:        3,000 tokens (pre-loaded)
├─ Dynamic search:       2,000 tokens (on-demand)
└─ Conversation:         1,500 tokens (remaining)

Les blocs mis en cache sont chargés au démarrage de la session. Les résultats de recherche dynamique comblent le budget restant au cas par cas. Cette approche hybride fournit à l’agent une base de contexte fréquemment nécessaire tout en préservant du budget pour des requêtes spécifiques.

Comparaison de la consommation de tokens

Sans mise en cache : chaque requête pertinente déclenche une recherche dans le coffre, renvoyant 1 500 à 2 000 tokens de contexte. Sur 10 requêtes dans une session, l’agent consomme 15 000 à 20 000 tokens de contexte provenant du coffre.

Avec mise en cache : trois blocs de contexte pré-construits consomment 4 500 tokens au total. Les recherches supplémentaires ajoutent 1 500 à 2 000 tokens par requête unique. Sur 10 requêtes dont 6 sont couvertes par les blocs mis en cache, l’agent consomme 4 500 + (4 × 1 500) = 10 500 tokens — soit environ la moitié de la consommation sans cache.


Hooks PostToolUse pour la compression de contexte

Les sorties des outils peuvent être verbeuses : traces de pile, listes de fichiers, résultats de tests. Un hook PostToolUse peut compresser ces sorties avant qu’elles ne consomment de l’espace dans la fenêtre de contexte.

Le problème

Un appel à l’outil Bash qui exécute des tests peut renvoyer :

PASSED tests/test_auth.py::test_login_success
PASSED tests/test_auth.py::test_login_failure
PASSED tests/test_auth.py::test_token_refresh
PASSED tests/test_auth.py::test_session_expiry
... (200 more lines)
FAILED tests/test_api.py::test_rate_limit_exceeded

La sortie complète fait 5 000 tokens, mais le signal utile tient en 2 lignes : 200 réussis, 1 échoué.

Implémentation du hook

#!/bin/bash
# ~/.claude/hooks/post-tool-use/compress-output.sh
# Compress verbose tool outputs to preserve context window

TOOL_NAME="$1"
OUTPUT="$2"
OUTPUT_LEN=${#OUTPUT}

# Only compress large outputs
if [ "$OUTPUT_LEN" -lt 2000 ]; then
    exit 0  # Pass through unchanged
fi

case "$TOOL_NAME" in
    Bash)
        # Compress test output
        if echo "$OUTPUT" | grep -q "PASSED\|FAILED"; then
            PASSED=$(echo "$OUTPUT" | grep -c "PASSED")
            FAILED=$(echo "$OUTPUT" | grep -c "FAILED")
            FAILURES=$(echo "$OUTPUT" | grep "FAILED")
            echo "Tests: $PASSED passed, $FAILED failed"
            if [ "$FAILED" -gt 0 ]; then
                echo "Failures:"
                echo "$FAILURES"
            fi
        fi
        ;;
esac

Prévention des déclenchements récursifs

Un hook de compression qui émet une sortie pourrait se déclencher lui-même s’il n’est pas protégé :

# Guard against recursive invocation
if [ -n "$COMPRESS_HOOK_ACTIVE" ]; then
    exit 0
fi
export COMPRESS_HOOK_ACTIVE=1

Heuristiques de compression

Type de sortie Détection Stratégie de compression
Résultats de tests Mots-clés PASSED / FAILED Compter les réussites/échecs, afficher uniquement les échecs
Listes de fichiers ls ou find dans la commande Tronquer aux 20 premières entrées + total
Traces de pile Mot-clé Traceback Conserver le premier et le dernier cadre + message d’erreur
Statut Git modified: / new file: Résumer les totaux par statut
Sortie de compilation warning: / error: Supprimer les lignes d’information, conserver les avertissements/erreurs

Pipeline d’intake et de triage des signaux

La couche d’intake détermine ce qui entre dans le coffre-fort. Sans curation, le coffre-fort accumule du bruit. Cette section couvre le pipeline de scoring qui achemine les signaux vers les dossiers de domaine.

Sources

Les signaux proviennent de multiples canaux :

  • Flux RSS : Blogs techniques, avis de sécurité, notes de version
  • Favoris : Favoris du navigateur sauvegardés via Obsidian Web Clipper ou bookmarklet
  • Newsletters : Extraits clés de newsletters par e-mail
  • Capture manuelle : Notes rédigées pendant la lecture, les conversations ou la recherche
  • Sorties d’outils : Sorties significatives d’outils IA capturées via des hooks

Dimensions de scoring

Chaque signal est évalué sur quatre dimensions (de 0.0 à 1.0 chacune) :

Dimension Question Score faible (0.0-0.3) Score élevé (0.7-1.0)
Pertinence Est-ce lié à mes domaines actifs ? Tangentiel, hors périmètre Directement pertinent pour le travail en cours
Actionnabilité Puis-je utiliser cette information ? Théorie pure, aucune application Technique ou pattern spécifique que je peux appliquer
Profondeur Quel est le niveau de substance du contenu ? Titres, résumé superficiel Analyse détaillée avec exemples
Autorité Quelle est la crédibilité de la source ? Blog anonyme, non vérifié Source primaire, évaluée par des pairs, expert reconnu

Score composite et routage

composite = (relevance * 0.35) + (actionability * 0.25) +
            (depth * 0.25) + (authority * 0.15)
Plage de score Action
0.55+ Routage automatique vers le dossier de domaine
0.40 - 0.55 Mise en file d’attente pour revue manuelle
< 0.40 Suppression (ne pas stocker)

Routage par domaine

Les signaux avec un score supérieur à 0.55 sont acheminés vers l’un des 12 dossiers de domaine en fonction de la correspondance de mots-clés et de la classification thématique :

05-signals/
├── ai-tooling/        # Claude, LLMs, AI development tools
├── security/          # Vulnerabilities, auth, cryptography
├── systems/           # Architecture, distributed systems
├── programming/       # Languages, patterns, algorithms
├── web/               # Frontend, backends, APIs
├── data/              # Databases, data engineering
├── devops/            # CI/CD, containers, infrastructure
├── design/            # UI/UX, product design
├── mobile/            # iOS, Android, cross-platform
├── career/            # Industry trends, hiring, growth
├── research/          # Academic papers, whitepapers
└── other/             # Signals that don't fit a domain

Statistiques de production

Sur 14 mois d’exploitation :

Métrique Valeur
Total de signaux traités 7 771
Routage automatique (>0.55) 4 832 (62 %)
En file d’attente pour revue (0.40-0.55) 1 543 (20 %)
Supprimés (<0.40) 1 396 (18 %)
Dossiers de domaine actifs 12
Moyenne de signaux par jour ~18

Patterns de graphe de connaissances

Le graphe de wiki-link d’Obsidian encode les relations entre les notes. Cette section couvre la sémantique des liens, la traversée de graphe pour l’expansion de contexte, et les anti-patterns qui dégradent la qualité du graphe.

Chaque wiki-link crée une arête dirigée dans le graphe. Obsidian suit à la fois les liens directs et les backlinks :

  • Lien direct : La note A contient [[Note B]] → A pointe vers B
  • Backlink : La note B indique que la note A la référence

Le graphe encode différents types de relations selon le contexte :

Pattern de lien Sémantique Exemple
Lien inline « Est lié à » “See [[OAuth Token Rotation]] for details”
Lien d’en-tête « A pour sous-thème » ”## Related\n- [[Token Rotation]]\n- [[Session Management]]”
Lien de type tag « Est catégorisé comme » ”[[type/reference]]”
Lien MOC « Fait partie de » Une note Maps of Content listant les notes associées

Maps of Content (MOCs)

Les MOCs sont des notes d’index qui organisent les notes associées en une structure navigable :

---
title: "Authentication & Security MOC"
type: moc
domain: security
---

## Core Concepts
- [[OAuth 2.0 Overview]]
- [[JWT Token Anatomy]]
- [[Session Management Patterns]]

## Implementation Patterns
- [[OAuth Token Rotation]]
- [[Refresh Token Security]]
- [[PKCE Flow Implementation]]

## Failure Modes
- [[Token Expiry Handling]]
- [[Session Fixation Prevention]]
- [[CSRF Defense Strategies]]

Les MOCs bénéficient à la recherche de deux façons :

  1. Correspondance directe. Une recherche pour « authentication overview » correspond au MOC lui-même, fournissant à l’agent une liste organisée de notes associées.
  2. Expansion de contexte. Après avoir trouvé une note spécifique, le système de recherche peut vérifier si la note apparaît dans des MOCs et inclure la structure du MOC dans les résultats, offrant à l’agent une carte du sujet plus large.

Traversée de graphe pour l’expansion de contexte

Une amélioration future du système de recherche : après avoir trouvé les meilleurs résultats, étendre le contexte en suivant les liens :

def expand_context(results, depth=1):
    """Follow wiki-links from top results to find related context."""
    expanded = set()
    for result in results:
        # Parse wiki-links from chunk text
        links = extract_wiki_links(result["chunk_text"])
        for link_target in links:
            # Resolve link to file path
            target_path = resolve_wiki_link(link_target)
            if target_path and target_path not in expanded:
                expanded.add(target_path)
                # Include target's most relevant chunk
                target_chunks = get_chunks_for_file(target_path)
                # ... rank and include best chunk
    return results + list(expanded_results)

Ceci n’est pas implémenté dans le système de recherche actuel, mais représente une extension naturelle de la structure de graphe.

Anti-patterns

Clusters orphelins. Groupes de notes qui se lient entre elles mais n’ont aucune connexion avec le reste du coffre-fort. Le panneau de graphe dans Obsidian les rend visibles sous forme d’îlots déconnectés. Les clusters orphelins indiquent des MOCs manquants ou des liens inter-domaines absents.

Prolifération de tags. Utiliser les tags de manière incohérente ou créer trop de tags trop granulaires. Un coffre-fort avec 500 tags uniques sur 5 000 notes donne en moyenne 1 note pour 10 tags — les tags ne sont pas utiles pour le filtrage. Consolidez vers 20 à 50 tags de haut niveau qui correspondent à vos dossiers de domaine.

Notes riches en liens, pauvres en contenu. Notes constituées uniquement de wiki-links sans prose. Ces notes s’indexent mal car le système de découpage n’a pas de texte à transformer en embeddings. Ajoutez au moins un paragraphe de contexte expliquant pourquoi les notes liées sont en rapport.

Liens bidirectionnels pour tout. Chaque référence n’a pas besoin d’être un wiki-link. Mentionner « OAuth » en passant ne nécessite pas [[OAuth 2.0 Overview]]. Réservez les wiki-links pour des relations intentionnelles et navigables où cliquer sur le lien fournirait un contexte utile.


Recettes de workflow pour développeurs

Workflows pratiques qui combinent la recherche dans le coffre-fort avec les tâches de développement quotidiennes.

Chargement de contexte matinal

Commencez la journée en chargeant le contexte pertinent :

Search my vault for notes about [current project] updated in the last week

Le système de recherche renvoie les notes récentes concernant votre projet actif, vous offrant un rafraîchissement rapide de là où vous en étiez. Plus efficace que de relire les messages de commit de la veille.

Capture de recherche pendant le développement

Pendant l’implémentation d’une fonctionnalité, capturez des informations sans quitter l’éditeur :

/capture "FastAPI dependency injection with async generators requires yield,
not return. The generator is the dependency lifecycle."
  --domain programming
  --tags fastapi,dependency-injection

L’information capturée est immédiatement indexée et disponible pour une recherche future. Au fil des mois, ces micro-captures construisent un corpus de connaissances spécifiques à l’implémentation.

Lancement de projet

Lors du démarrage d’un nouveau projet ou d’une nouvelle fonctionnalité :

  1. Rechercher dans le coffre-fort : « Qu’est-ce que je sais sur [technologie/pattern] ? »
  2. Examiner les 5 meilleurs résultats pour les décisions antérieures et les pièges
  3. Vérifier si un MOC existe pour le domaine ; sinon, en créer un
  4. Rechercher les modes de défaillance : « problèmes avec [technologie] »

Débogage avec la recherche dans le coffre-fort

Face à une erreur ou un comportement inattendu :

Search my vault for [error message or symptom]

Les notes de débogage antérieures contiennent souvent la cause racine et le correctif. Ceci est particulièrement précieux pour les problèmes récurrents entre projets — le coffre-fort se souvient de ce que vous oubliez.

Préparation de revue de code

Avant de réviser une PR :

Search my vault for patterns and conventions about [module being changed]

Le coffre-fort renvoie les décisions antérieures, les contraintes architecturales et les standards de code pertinents pour le code en cours de revue. La revue est informée par la connaissance institutionnelle, pas seulement par le diff.

Optimisation des performances

Cette section couvre les stratégies d’optimisation pour différentes tailles de coffre-fort et différents modes d’utilisation.

Gestion de la taille de l’index

Taille du coffre-fort Fragments Taille BDD Réindexation complète Incrémentale
500 notes ~1 500 3 Mo 15 secondes <1 seconde
2 000 notes ~6 000 12 Mo 45 secondes 2 secondes
5 000 notes ~15 000 30 Mo 2 minutes 4 secondes
15 000 notes ~50 000 83 Mo 4 minutes <10 secondes
50 000 notes ~150 000 250 Mo 15 minutes 30 secondes

Au-delà de 50 000 notes, envisagez : - D’augmenter la taille des lots de 64 à 128 pour accélérer la génération d’embeddings - D’utiliser le mode WAL (par défaut) pour l’accès concurrent - D’exécuter la réindexation complète en dehors des heures d’utilisation

Optimisation des requêtes

Mode WAL. Le mode Write-Ahead Logging de SQLite permet des lectures concurrentes pendant que l’indexeur écrit :

db.execute("PRAGMA journal_mode=WAL")

C’est essentiel lorsque le serveur MCP traite des requêtes pendant que l’indexeur exécute une mise à jour incrémentale.

Réutilisation des connexions. Le serveur MCP devrait réutiliser les connexions à la base de données plutôt que d’ouvrir une nouvelle connexion par requête. Une connexion unique et persistante avec le mode WAL prend en charge les lectures concurrentes.

# MCP server initialization
db = sqlite3.connect(DB_PATH, check_same_thread=False)
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA mmap_size=268435456")  # 256 MB mmap

E/S mappées en mémoire. Le pragma mmap_size indique à SQLite d’utiliser des E/S mappées en mémoire pour le fichier de base de données. Pour une base de 83 Mo, mapper l’intégralité du fichier en mémoire élimine la plupart des lectures disque.

Optimisation FTS5. Après une réindexation complète, exécutez :

INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

Cela fusionne les segments internes de l’arbre B de FTS5, réduisant la latence des requêtes pour les recherches ultérieures.

Benchmarks de mise à l’échelle

Mesuré sur Apple M3 Pro, 36 Go de RAM, SSD NVMe :

Opération 500 notes 5K notes 15K notes 50K notes
Requête BM25 2ms 5ms 12ms 25ms
Requête vectorielle 1ms 3ms 8ms 20ms
Fusion RRF <1ms <1ms 3ms 5ms
Recherche complète 3ms 8ms 23ms 50ms

Tous les benchmarks incluent l’accès à la base de données, l’exécution des requêtes et le formatage des résultats. La latence réseau pour la communication MCP STDIO ajoute 1 à 2 ms.


Dépannage

Désynchronisation de l’index

Symptôme : La recherche renvoie des résultats obsolètes ou ne trouve pas les notes récemment ajoutées.

Cause : L’indexeur incrémental ne s’est pas exécuté après l’ajout de notes, ou le mtime d’un fichier n’a pas été mis à jour (par exemple, synchronisé depuis une autre machine avec les horodatages préservés).

Solution : Lancez une réindexation complète : python index_vault.py --full

Changement de modèle d’embeddings

Symptôme : Après avoir changé le modèle d’embeddings, la recherche vectorielle renvoie des résultats incohérents.

Cause : Les anciens vecteurs (issus du modèle précédent) sont comparés aux nouveaux vecteurs de requête. Les dimensions ou la sémantique de l’espace vectoriel sont incompatibles.

Solution : L’indexeur devrait détecter la différence de hash du modèle et déclencher automatiquement une réindexation complète. Si ce n’est pas le cas, supprimez manuellement la base de données et réindexez :

rm vectors.db
python index_vault.py --full

Maintenance FTS5

Symptôme : Les requêtes FTS5 renvoient des résultats incorrects ou incomplets après de nombreuses mises à jour incrémentales.

Cause : Les segments internes de FTS5 peuvent se fragmenter après de nombreuses petites mises à jour.

Solution : Reconstruisez et optimisez :

INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild');
INSERT INTO chunks_fts(chunks_fts) VALUES('optimize');

Délai d’expiration MCP

Symptôme : L’outil d’IA signale que le serveur MCP a expiré.

Cause : La première requête déclenche le chargement du modèle (initialisation paresseuse), ce qui prend 2 à 5 secondes. Le délai d’expiration MCP par défaut de l’outil d’IA peut être plus court.

Solution : Préchauffez le modèle au démarrage du serveur :

# In MCP server initialization
retriever = HybridRetriever(db_path, vault_path)
retriever.search("warmup", limit=1)  # Trigger model load

Verrouillage de fichiers SQLite

Symptôme : Erreurs SQLITE_BUSY ou SQLITE_LOCKED.

Cause : Plusieurs processus écrivent simultanément dans la base de données. Le mode WAL autorise les lectures concurrentes mais un seul processus en écriture.

Solution : Assurez-vous qu’un seul processus (l’indexeur) écrit dans la base de données. Le serveur MCP et les hooks ne doivent effectuer que des lectures. Si vous avez besoin d’écritures concurrentes, utilisez le mode WAL et définissez un délai d’attente :

db.execute("PRAGMA busy_timeout=5000")  # Wait up to 5 seconds

sqlite-vec ne se charge pas

Symptôme : La recherche vectorielle est désactivée ; le système de recherche fonctionne en mode BM25 uniquement.

Cause : L’extension sqlite-vec n’est pas installée, n’est pas trouvée dans le chemin de la bibliothèque, ou est incompatible avec la version de SQLite.

Solution :

# Install via pip
pip install sqlite-vec

# Or compile from source
git clone https://github.com/asg017/sqlite-vec
cd sqlite-vec && make

Vérifiez que l’extension se charge correctement :

import sqlite3
db = sqlite3.connect(":memory:")
db.enable_load_extension(True)
db.load_extension("vec0")
print("sqlite-vec loaded successfully")

Problèmes de mémoire avec les grands coffres-forts

Symptôme : Erreurs de mémoire insuffisante lors de la réindexation complète d’un grand coffre-fort (50 000+ notes).

Cause : La taille des lots d’embeddings est trop importante, ou le contenu de tous les fichiers est chargé en mémoire simultanément.

Solution : Réduisez la taille des lots et traitez les fichiers de manière incrémentale :

BATCH_SIZE = 32  # Reduce from 64

Assurez-vous également que l’indexeur traite les fichiers un par un (lecture, découpage en fragments et génération d’embeddings pour chaque fichier avant de passer au suivant) plutôt que de charger tous les fichiers en mémoire.


Guide de migration

Depuis Apple Notes

  1. Exportez Apple Notes via l’option « Tout exporter » (macOS) ou utilisez un outil de migration comme apple-notes-liberator
  2. Convertissez les exports HTML en markdown avec markdownify ou pandoc
  3. Déplacez les fichiers convertis dans le dossier 00-inbox/ de votre coffre-fort
  4. Vérifiez et ajoutez le frontmatter à chaque note
  5. Déplacez les notes dans les dossiers de domaine appropriés

Depuis Notion

  1. Exportez depuis Notion : Paramètres → Exporter → Markdown & CSV
  2. Décompressez l’export dans le dossier 00-inbox/ de votre coffre-fort
  3. Corrigez les artefacts markdown spécifiques à Notion :
  4. Notion utilise - [ ] pour les listes de tâches — c’est du markdown standard
  5. Notion inclut des tables de propriétés en HTML — convertissez-les en frontmatter YAML
  6. Notion intègre les images avec des chemins relatifs — copiez les images dans votre dossier de pièces jointes
  7. Ajoutez le frontmatter standard (type, domain, tags)
  8. Remplacez les liens de pages Notion par des wiki-links Obsidian

Depuis Google Docs

  1. Utilisez Google Takeout pour exporter tous les documents
  2. Convertissez les fichiers .docx en markdown : pandoc -f docx -t markdown input.docx -o output.md
  3. Conversion par lot : for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done
  4. Déplacez les fichiers dans le coffre-fort, ajoutez le frontmatter, organisez dans des dossiers

Depuis du Markdown brut (sans Obsidian)

Si vous disposez déjà d’un répertoire de fichiers markdown :

  1. Ouvrez le répertoire comme coffre-fort Obsidian (Obsidian → Ouvrir un coffre-fort → Ouvrir un dossier)
  2. Ajoutez .obsidian/ au .gitignore si le répertoire est versionné
  3. Créez des modèles de frontmatter et appliquez-les aux fichiers existants
  4. Commencez à lier les notes avec des [[wiki-links]] au fur et à mesure de votre lecture et de votre organisation
  5. 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 :

  1. 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.
  2. Migrez le contenu, pas l’index. Les fichiers du coffre-fort sont la source de vérité. L’index est un artefact dérivé.
  3. Vérifiez après la migration. Exécutez 10 à 20 requêtes dont vous connaissez les réponses et vérifiez que les résultats correspondent à vos attentes.

Journal des modifications

Date Modification
2026-03-03 Mise à jour de l’évolution de la spécification MCP (novembre 2025 : Streamable HTTP, .well-known, annotations d’outils). Ajout du fine-tuning Model2Vec et du support du tokenizer BPE/Unigram. Ajout du tableau comparatif des serveurs MCP communautaires. Mise à jour de Smart Connections vers la v4.
2026-03-02 Ajout de potion-base-32M et potion-retrieval-32M à la comparaison des modèles. Ajout de la section quantification/réduction de dimensionnalité. Ajout de la note sur l’évolution de la spécification MCP.
2026-03-01 Publication initiale

Références


  1. Cormack, G.V., Clarke, C.L.A., and Buettcher, S. Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods. SIGIR, 2009. Introduit RRF avec k=60 comme méthode sans paramètre pour combiner des listes classées. 

  2. OpenAI Embeddings Pricing. text-embedding-3-small : 0,02 $ par million de tokens. Coût estimé du coffre-fort par réindexation complète : ~0,30 $. 

  3. 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. 

  4. 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 %). 

  5. SQLite FTS5 Extension. FTS5 fournit la recherche en texte intégral avec classement BM25 et pondération configurable des colonnes. 

  6. sqlite-vec: A vector search SQLite extension. Fournit des tables virtuelles vec0 pour la recherche vectorielle KNN au sein de SQLite. 

  7. Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval, 2009. 

  8. 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. 

  9. Reimers, N. and Gurevych, I. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks. EMNLP, 2019. Travaux fondateurs sur la similarité sémantique dense. 

  10. Luan, Y. et al. Sparse, Dense, and Attentional Representations for Text Retrieval. TACL, 2021. La recherche hybride (hybrid retrieval) surpasse systématiquement les approches à modalité unique sur MS MARCO. 

  11. SQLite Write-Ahead Logging. Mode WAL pour les lectures concurrentes avec un seul processus d’écriture. 

  12. 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). 

  13. Thakur, N. et al. BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models. NeurIPS, 2021. 

  14. Model2Vec: Distill a Small Fast Model from any Sentence Transformer. Minish Lab, 2024. 

  15. Obsidian Documentation. Documentation officielle d’Obsidian. 

  16. Model Context Protocol Specification. Le standard MCP pour connecter les outils d’IA aux sources de données. 

  17. 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()

  18. Model2Vec Potion Models. Minish Lab, 2025. Potion-base-32M (MTEB 52,46), potion-retrieval-32M (MTEB retrieval 36,35), et fonctionnalités de quantification/réduction de dimensionnalité à partir de la v0.5.0+. 

  19. Update on the Next MCP Protocol Release. La version de novembre 2025 a introduit le transport Streamable HTTP, la découverte par URL .well-known, les annotations structurées d’outils et la standardisation des niveaux SDK. La prochaine version, provisoirement prévue pour mi-2026, inclura les opérations asynchrones, les extensions spécifiques aux domaines et la communication entre agents. 

  20. Model2Vec Releases. v0.4.0 (fév. 2025) : prise en charge de l’entraînement et du fine-tuning. v0.5.0 (avr. 2025) : réécriture du backend, quantification, réduction de dimensionnalité. v0.7.0 (oct. 2025) : quantification du vocabulaire, prise en charge des tokeniseurs BPE/Unigram. 

  21. Smart Connections for Obsidian. Smart Connections v4 : embeddings IA en local d’abord, la recherche sémantique fonctionne hors ligne après l’indexation initiale. 

VAULT obsidian.md INDEXED