obsidian:~/vault$ search --hybrid obsidian

Example vault location

#

words: 11484 read_time: 58m updated: 2026-03-02 07:53
$ 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 : premier coffre-fort connecté à l’IA

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

Prérequis

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

Étape 1 : créer un coffre-fort

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

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

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

Étape 2 : installer un serveur MCP

Le serveur communautaire obsidian-mcp offre un accès immédiat au coffre-fort. Installez-le :

npm install -g obsidian-mcp-server

Étape 3 : configurer votre outil d’IA

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

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

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

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

Cursor — ajoutez dans .cursor/mcp.json :

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

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

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

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

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

Ce que vous venez de construire

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

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

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


Cadre de décision : Obsidian face aux alternatives

Obsidian ne convient pas à tous les cas d’utilisation. Cette section définit quand Obsidian est le bon substrat, quand il est surdimensionné et quand autre chose convient mieux.

Arbre de décision

START: What is your primary content type?

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

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

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

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

Matrice de comparaison

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

Quand Obsidian est surdimensionné

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

Quand Obsidian est le bon choix

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

Modèle mental : trois couches

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

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

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

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

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

Les couches sont découplées par conception. Le pipeline de notation de l’intake ne connaît rien des embeddings. Le système de récupération ne connaît rien des règles de routage des signaux. Le serveur MCP ne sait pas comment les notes ont été créées. Ce découplage signifie que vous pouvez améliorer chaque couche indépendamment. Remplacez le modèle d’embeddings sans modifier le pipeline d’intake. Ajoutez une nouvelle fonctionnalité MCP sans modifier le système de récupération. Changez les heuristiques de notation des signaux sans toucher à l’index.


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

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

Structure des dossiers

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

vault/
├── 00-inbox/              # Captures non triées, en attente de classement
├── 01-projects/           # Notes de projets actifs
├── 02-areas/              # Domaines de responsabilité continus
├── 03-resources/          # Matériel de référence par sujet
   ├── programming/
   ├── security/
   ├── ai-engineering/
   ├── design/
   └── devops/
├── 04-archive/            # Projets terminés, anciennes références
├── 05-signals/            # Signaux entrants notés
   ├── ai-tooling/
   ├── security/
   ├── systems/
   └── ...12 domain folders
├── 06-daily/              # Notes quotidiennes (si utilisées)
├── 07-templates/          # Modèles de notes (exclus de l'index)
├── 08-attachments/        # Images, PDF (exclus de l'index)
├── .obsidian/             # Configuration Obsidian (exclue de l'index)
└── .indexignore            # Chemins à exclure de l'index de recherche

Dossiers qui doivent être indexés : tout ce qui contient de la prose en markdown — projets, domaines, ressources, signaux, notes quotidiennes.

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

Le fichier .indexignore

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

# Obsidian internal
.obsidian/

# Templates contain placeholders, not content
07-templates/

# Binary attachments
08-attachments/

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

# Financial records
02-areas/finance/personal/

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

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

Schéma des notes

Chaque note doit comporter un YAML frontmatter. Le système de recherche utilise les champs frontmatter pour le filtrage et l’enrichissement du contexte :

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

Champs requis pour la recherche :

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

Champs optionnels mais précieux :

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

Conventions de découpage

Le système de recherche découpe aux limites des titres H2 (##). Cela signifie que la structure de vos notes affecte directement la granularité de la recherche :

Bon pour la recherche :

## Token Rotation Strategy

The rotation interval depends on the threat model...

## Implementation with refresh_token

The OAuth 2.0 refresh token flow requires...

## Error Handling: Expired Tokens

When a token expires mid-request...

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

Mauvais pour la recherche :

# OAuth Notes

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

Une longue section sans titres H2 produit un seul grand fragment. L’embedding fait la moyenne de tous les sujets de la section. Une requête sur n’importe quel sous-sujet correspond à la note entière de manière égale.

Règle générale : si une section couvre plus d’un concept, divisez-la en sous-sections H2. Le système de découpage s’occupe du reste.

Ce qu’il ne faut pas mettre dans les notes

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

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

Écosystème de plugins pour les workflows IA

Les plugins Obsidian qui améliorent la qualité du coffre-fort pour la recherche par IA se répartissent en trois catégories : structurels (imposent la cohérence), d’interrogation (exposent les métadonnées) et de synchronisation (maintiennent le coffre-fort à jour).

Plugins essentiels

Dataview. Interroge votre coffre-fort comme une base de données en utilisant les champs frontmatter. Créez des index dynamiques : « toutes les notes taguées security mises à jour au cours des 30 derniers jours » ou « toutes les notes de projet avec le statut active. » Dataview n’aide pas directement la recherche, mais vous aide à identifier les lacunes dans la couverture de votre coffre-fort et à trouver les notes qui nécessitent une mise à jour.

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

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

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

## Key Points

## Details

## Références

**Linter.** Applique des règles de formatage dans l'ensemble du coffre-fort. Une hiérarchie de titres cohérente (H1 pour le titre, H2 pour les sections, H3 pour les sous-sections) garantit que le découpeur produit des résultats prévisibles. Règles du Linter importantes pour la recherche :

- Incrémentation des titres : appliquer des niveaux de titres séquentiels (pas de saut de H1 à H3)
- Titre YAML : correspondre au nom du fichier
- Espaces en fin de ligne : supprimer (évite les artefacts de tokenisation FTS5)
- Lignes vides consécutives : limiter à 1 (découpage plus propre)

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

### Plugins qui améliorent l'indexation

**Smart Connections.** Un plugin Obsidian qui fournit une recherche sémantique alimentée par l'IA directement dans Obsidian. Il crée son propre index d'embeddings. Bien que le système de recherche décrit dans ce guide soit externe à Obsidian (il s'exécute en tant que pipeline Python), Smart Connections est utile pour explorer les relations sémantiques pendant l'écriture. Les deux systèmes indexent le même contenu mais répondent à des cas d'usage différents : Smart Connections pour la découverte dans l'éditeur, le retriever externe pour l'intégration avec les outils d'IA.

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

### Plugins qui nuisent à l'indexation

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

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

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

### Configuration des plugins qui compte

**Récupération de fichiers → Activée.** Protège contre la suppression accidentelle de notes. Pas directement lié à la recherche, mais critique pour une base de connaissances dont vous dépendez.

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

**Emplacement par défaut des nouveaux fichiers → Dossier désigné.** Dirigez les nouveaux fichiers vers `00-inbox/` pour que les notes non catégorisées ne polluent pas les dossiers de domaine. La boîte de réception est une zone de transit ; les fichiers sont déplacés vers les dossiers de domaine après le tri.

**Format wiki-link → Chemin le plus court possible.** Des cibles de liens plus courtes sont plus faciles à résoudre pour le retriever lors de l'indexation de la structure des liens.

---

## Modèles d'embeddings : choix et configuration

Le modèle d'embeddings convertit les chunks de texte en vecteurs numériques pour la recherche sémantique. Le choix du modèle détermine la qualité de la recherche, la taille de l'index, la vitesse d'embedding et les dépendances d'exécution. Cette section explique pourquoi potion-base-8M de Model2Vec est le choix par défaut et quand choisir des alternatives.

### Pourquoi Model2Vec potion-base-8M

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

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

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

### Configuration

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

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

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

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

Chargement paresseux. Le modèle se charge à la première utilisation, pas au moment de l’import. Importer le module embedder ne coûte rien lorsque le retriever fonctionne en mode de repli BM25 uniquement (par exemple, lorsque le venv d’embeddings n’est pas installé).

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

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

Traitement par lots. L’embedder traite les textes par lots de 64 pour amortir le surcoût de Model2Vec. L’indexeur alimente embed_batch() avec des chunks plutôt que d’encoder un chunk à la fois.

Quand choisir des alternatives

Modèle Dim Taille Vitesse Qualité (MTEB) Idéal pour
potion-base-8M 256 30 Mo 500x 50,03 Par défaut : local, rapide, pas de GPU
all-MiniLM-L6-v2 384 80 Mo 1x 56,09 Qualité supérieure, toujours local
nomic-embed-text-v1.5 768 270 Mo 0,5x 62,28 Meilleure qualité locale
text-embedding-3-small 1536 API N/A 62,30 Basé sur API, qualité maximale

Choisissez all-MiniLM-L6-v2 lorsque la qualité de recherche compte plus que la vitesse et que vous avez PyTorch installé. Les vecteurs de 384 dimensions augmentent la taille de la base de données SQLite d’environ 50 % par rapport aux vecteurs de 256 dimensions. La vitesse d’embedding passe de moins d’une minute à environ 10 minutes pour une réindexation complète de 15 000 fichiers sur du matériel M-series.

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

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

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

Suivi du hash du modèle

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

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

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

Modes de défaillance

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

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

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


Recherche plein texte avec FTS5

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

Table virtuelle FTS5

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

Mode content-sync. Le paramètre content=chunks indique à FTS5 de référencer directement la table chunks plutôt que de stocker une copie dupliquée du texte. Cela réduit de moitié les besoins en stockage, mais implique que FTS5 doit être synchronisé manuellement lorsque des chunks sont insérés, mis à jour ou supprimés.

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

Classement BM25

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

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

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

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

Quand BM25 l’emporte

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

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

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

Quand BM25 échoue

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

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

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

Tokeniseur FTS5

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

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

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

Maintenance

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

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

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


Recherche vectorielle avec sqlite-vec

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

Table virtuelle sqlite-vec

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

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

Pipeline d’embeddings

Le pipeline va de la note au vecteur interrogeable :

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

Sérialisation des vecteurs

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

import struct

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

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

Requête KNN

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

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

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

    return results

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

Quand la recherche vectorielle l’emporte

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

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

Quand la recherche vectorielle échoue

La recherche vectorielle peine avec les identifiants exacts :

  • Requête : _rrf_fuse → Renvoie des notes sur « fusion algorithms » et « rank merging » mais peut classer la définition réelle de la fonction plus bas que des discussions conceptuelles
  • Requête : PostToolUse → Renvoie des notes sur « tool lifecycle hooks » et « post-execution handlers » plutôt que le nom spécifique du hook

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

Dégradation gracieuse

Si sqlite-vec ne parvient pas à se charger (extension manquante, plateforme incompatible, bibliothèque corrompue), le récupérateur bascule vers une recherche BM25 uniquement :

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

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

Le récupérateur vérifie vec_available avant de tenter des requêtes vectorielles. Lorsqu’il est désactivé, toutes les recherches utilisent BM25 uniquement, et l’étape de fusion RRF est ignorée.


Reciprocal Rank Fusion (RRF)

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

L’algorithme

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

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

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

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

Pourquoi RRF plutôt que les alternatives

La combinaison linéaire pondérée nécessite de calibrer les scores BM25 par rapport aux distances cosine. Les scores BM25 sont non bornés et varient avec la taille du corpus. Les distances cosine sont bornées [0, 2]. Les combiner requiert une normalisation, et les paramètres de normalisation dépendent du jeu de données. RRF n’utilise que les positions de rang, qui sont toujours des entiers commençant à 1, quelle que soit la méthode de notation.

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

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

La fusion en pratique

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

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

Fragment BM25 Vec Score fusionné
review-aggregator.py « Disagreement Resolution » #3 #1 0,0323
code-review-patterns.md « Multi-Reviewer » #4 #2 0,0317
deliberation-config.json « Review Weights » #1 0,0164

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

Pour la trace complète étape par étape avec le calcul RRF par rang, essayez différentes valeurs de k dans le calculateur RRF interactif.

Implémentation

RRF_K = 60

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

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

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

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

Réglage de k

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

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

Commencez avec k=60. L’article original sur RRF a montré que cette valeur est robuste sur des jeux de données TREC variés. Ne l’ajustez qu’après avoir mesuré les cas d’échec sur votre propre distribution de requêtes.

Résolution des égalités

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

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

Le pipeline de recherche complet

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

Flux de bout en bout

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

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

La API de recherche

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

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

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

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

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

        return results

Troncature par budget de tokens

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

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

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

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

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

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

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

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

Chemin de dégradation progressive

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

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

Statistiques de production

Mesurées sur un coffre-fort de 16 894 fichiers, 49 746 fragments, une base de données SQLite de 83 Mo, Apple M3 Pro :

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

Hachage du contenu et détection des modifications

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

Comparaison des dates de modification des fichiers

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

  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 hachage du contenu

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

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

Gestion des suppressions

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

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

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

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


Réindexation incrémentale vs complète

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

Réindexation incrémentale

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

Ce qu’il fait : 1. Analyse le vault à la recherche de fichiers modifiés (comparaison de mtime) 2. Supprime les chunks des fichiers supprimés 3. Re-découpe et ré-encode les fichiers modifiés 4. Insère de nouveaux chunks pour les nouveaux fichiers 5. Synchronise l’index FTS5

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

python index_vault.py --incremental

Réindexation complète

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

Ce qu’il fait : 1. Supprime toutes les données existantes (chunks, vecteurs, entrées FTS5) 2. Analyse l’intégralité du vault 3. Découpe tous les fichiers en chunks 4. Encode tous les chunks en embeddings 5. Reconstruit l’index FTS5 à partir de zéro

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

python index_vault.py --full

Idempotence

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

Récupération après corruption

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

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

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

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

Le flag --incremental

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

  1. Vérification du hash du modèle. Compare le hash du modèle stocké avec le modèle actuel. En cas de différence, bascule automatiquement en mode de réindexation complète et avertit l’utilisateur.
  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. Re-découpe et ré-encode les 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 SIGINT en terminant le fichier en cours avant de s’arrêter.

Filtrage des identifiants et périmètre des données

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

Le problème

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

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

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

Filtrage par motifs

Le filtre d’identifiants s’exécute sur chaque chunk avant le stockage, en correspondant 25 motifs spécifiques à des fournisseurs ainsi que des motifs génériques :

Motifs spécifiques aux fournisseurs :

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

Motifs génériques :

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

Implémentation du filtre

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

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

    return text, result

Choix de conception clés :

  1. Filtrer avant l’encodage. Le texte nettoyé est celui qui est encodé en embeddings. La représentation vectorielle n’encode jamais les motifs d’identifiants. Une requête sur « clé API » renvoie les notes qui traitent de la gestion des clés API, et non les notes qui contiennent de véritables clés.

  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 était présent ici » sans encoder l’identifiant lui-même.

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

Exclusion par chemin

Le fichier .indexignore fournit une exclusion grossière par chemin. Le filtre d’identifiants fournit un nettoyage fin au sein des fichiers indexés. Les deux sont nécessaires :

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

Classification des données

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

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

Architecture du serveur MCP

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

Choix du protocole : STDIO vs HTTP

MCP prend en charge deux modes de transport :

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

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

HTTP — Le serveur MCP s’exécute en tant que service HTTP autonome. Utile pour l’accès à distance, les configurations multi-clients ou les configurations d’équipe où le coffre se trouve sur un serveur partagé.

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

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

Conception des fonctionnalités

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

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

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

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

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

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

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

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

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

Limites de permissions

Le serveur MCP devrait imposer des limites strictes :

  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 distincts, et non par le serveur MCP.

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

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

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

Gestion des erreurs

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

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

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

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

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

Intégration avec Claude Code

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

Configuration MCP

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

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

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

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

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

Intégration des hooks

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

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

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

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

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

Hook PostToolUse — Capture les sorties significatives des outils et les renvoie dans le coffre pour une recherche future.

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

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

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

Le pattern obsidian_bridge.py

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

# obsidian_bridge.py
from retriever import HybridRetriever

_retriever = None

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

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

    if not results:
        return ""

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

    return "\n".join(lines)

Le skill /capture

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

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

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

Gestion de la fenêtre de contexte

L’intégration doit tenir compte de la fenêtre de contexte de Claude Code :

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

Intégration avec Codex CLI

Codex CLI se connecte aux serveurs MCP via config.toml. Le pattern d’intégration diffère de Claude Code par la syntaxe de configuration et la méthode de transmission des instructions.

Configuration MCP

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

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

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

Patterns AGENTS.md

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

## Outils disponibles

### Coffre-fort Obsidian (MCP : obsidian)
Utilisez l'outil `obsidian_search` pour trouver du contexte pertinent dans la base de connaissances.
Effectuez une recherche dans le coffre-fort lorsque vous avez besoin de :
- Contexte sur un concept ou un patron de conception
- Décisions antérieures ou justifications
- Documentation de référence pour l'implémentation

Exemples de requêtes :
- "authentication patterns in FastAPI"
- "how does the review aggregator work"
- "sqlite-vec configuration"

Différences avec Claude Code

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

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


Cursor et autres outils

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

Cursor

Ajoutez ceci au fichier .cursor/mcp.json à la racine de votre projet :

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

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

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

Matrice de compatibilité

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

Solution de repli pour les outils sans MCP

Pour les outils qui ne prennent pas en charge MCP, le moteur de recherche peut être encapsulé en tant que CLI :

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

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

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


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

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

Le principe

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

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

Invalidation du cache

L’invalidation du cache repose sur deux signaux :

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

Gestion du budget de tokens

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

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

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

Comparaison de la consommation de tokens

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

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


Hooks PostToolUse pour la compression de contexte

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

Le problème

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

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

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

Implémentation du hook

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

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

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

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

Prévention des déclenchements récursifs

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

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

Heuristiques de compression

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

Pipeline d’intake et de triage des signaux

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

Sources

Les signaux proviennent de multiples canaux :

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

Dimensions de notation

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

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

Score composite et routage

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

Routage par domaine

Les signaux dépassant 0.55 sont acheminés vers l’un des 12 dossiers de domaine en fonction de la correspondance par mots-clés et de la classification thématique :

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

Statistiques de production

Sur 14 mois d’exploitation :

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

Patterns de graphe de connaissances

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

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

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

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

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

Maps of Content (MOCs)

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

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

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

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

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

Les MOCs bénéficient à la recherche de deux manières :

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

Traversée de graphe pour l’expansion du contexte

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

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

Cette fonctionnalité n’est pas implémentée dans le retriever actuel mais représente une extension naturelle de la structure de graphe.

Anti-patterns

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

Prolifération de tags. Utilisation incohérente des tags ou création de tags trop granulaires. Un vault avec 500 tags uniques sur 5 000 notes affiche une moyenne d’une note pour 10 tags — les tags ne sont pas utiles pour le filtrage. Consolidez vers 20 à 50 tags de haut niveau correspondant à vos dossiers de domaine.

Notes riches en liens, pauvres en contenu. Notes constituées entièrement de wiki-links sans prose. Ces notes s’indexent mal car le chunker n’a aucun texte à convertir en embeddings. Ajoutez au minimum un paragraphe de contexte expliquant pourquoi les notes liées sont en rapport.

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


Recettes de workflow pour développeurs

Workflows pratiques combinant la recherche dans le vault avec les tâches de développement quotidiennes.

Chargement du contexte matinal

Commencez la journée en chargeant le contexte pertinent :

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

Le retriever renvoie les notes récentes concernant votre projet actif, vous offrant un rappel rapide de l’état d’avancement. Plus efficace que de relire les messages de commit de la veille.

Capture de recherche pendant le développement

Pendant l’implémentation d’une fonctionnalité, capturez les insights sans quitter l’éditeur :

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

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

Lancement de projet

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

  1. Recherchez dans le vault : « Que sais-je sur [technologie/pattern] ? »
  2. Examinez les 5 premiers résultats pour les décisions antérieures et les pièges connus
  3. Vérifiez si un MOC existe pour le domaine ; sinon, créez-en un
  4. Recherchez les modes de défaillance : « problèmes avec [technologie] »

Débogage avec la recherche dans le vault

Face à une erreur ou un comportement inattendu :

Search my vault for [error message or symptom]

Les notes de débogage antérieures contiennent souvent la cause racine et le correctif. Cela s’avère particulièrement précieux pour les problèmes récurrents entre projets — le vault se souvient de ce que vous oubliez.

Préparation de revue de code

Avant de réviser une PR :

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

Le vault renvoie les décisions antérieures, les contraintes architecturales et les standards de code pertinents pour le code en cours de revue. La revue s’appuie sur la connaissance institutionnelle, pas uniquement sur le diff.

Optimisation des performances

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

Gestion de la taille de l’index

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

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

Optimisation des requêtes

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

db.execute("PRAGMA journal_mode=WAL")

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

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

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

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

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

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

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

Benchmarks de mise à l’échelle

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

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

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


Dépannage

Dérive de l’index

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

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

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

Changement de modèle d’embeddings

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

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

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

rm vectors.db
python index_vault.py --full

Maintenance FTS5

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

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

Solution : reconstruisez et optimisez :

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

Délai d’expiration MCP

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

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

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

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

Verrous de fichier SQLite

Symptôme : erreurs SQLITE_BUSY ou SQLITE_LOCKED.

Cause : plusieurs processus écrivent simultanément dans la base de données. Le mode WAL permet des lectures concurrentes mais n’autorise qu’un seul écrivain.

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

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

sqlite-vec ne se charge pas

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

Cause : l’extension sqlite-vec n’est pas installée, introuvable dans le chemin des bibliothèques, ou incompatible avec la version de SQLite.

Solution :

# Install via pip
pip install sqlite-vec

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

Vérifiez que l’extension se charge correctement :

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

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

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

Cause : la taille des lots d’embeddings est trop importante, ou tous les contenus de fichiers sont chargés en mémoire simultanément.

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

BATCH_SIZE = 32  # Reduce from 64

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


Guide de migration

Depuis Apple Notes

  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 contrôle — c’est du markdown standard
  5. Notion inclut les tableaux de propriétés en HTML — convertissez-les en frontmatter YAML
  6. Notion intègre les images sous forme de 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 lots : for f in *.docx; do pandoc -f docx -t markdown "$f" -o "${f%.docx}.md"; done
  4. Déplacez dans le coffre-fort, ajoutez le frontmatter, organisez en dossiers

Depuis du markdown brut (sans Obsidian)

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

  1. Ouvrez le répertoire en tant que coffre-fort Obsidian (Obsidian → Ouvrir un coffre-fort → Ouvrir un dossier)
  2. Ajoutez .obsidian/ à .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 fil 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
01 mars 2026 Version initiale

Références


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

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

  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 plein texte avec classement BM25 et pondérations de colonnes configurables. 

  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. et 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. et 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 unimodales sur MS MARCO. 

  11. SQLite Write-Ahead Logging. Le mode WAL pour des lectures concurrentes avec un seul écrivain. 

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

VAULT obsidian.md INDEXED