feat(search): recherche semantique - embeddings vectoriels + hybride RRF (#70)

This commit is contained in:
2026-09-12 00:12:20 -04:00
parent 45be3125de
commit 31d4be8015
19 changed files with 1388 additions and 90 deletions
+5 -25
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.2.1 | **Dernière mise à jour :** 2026-09-11
> **Version :** 2.3.0-dev | **Dernière mise à jour :** 2026-09-12
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
@@ -44,27 +44,6 @@
## ⚪ Backlog — Priorité 4 (P4)
### 70. Recherche sémantique — Embeddings vectoriels
- **Effort :** 4-5 jours | **Impact :** 🟢
- **Description :** La recherche actuelle (TF-IDF) ne trouve que les documents contenant EXACTEMENT les mots tapés. La recherche sémantique comprend le SENS de la requête et trouve des documents pertinents même s'ils utilisent des mots différents.
- **Exemple concret :** Vous cherchez « comment sauvegarder mes données ». La recherche TF-IDF ne trouvera que les documents contenant « sauvegarder » ET « données ». La recherche sémantique trouvera aussi un document titré « Stratégie de backup automatique » ou « Protection contre la perte de fichiers » parce qu'elle comprend que ces phrases parlent de la même chose.
- **Fonctionnement technique :**
- Chaque document (ou chunk de ~512 tokens) est converti en un **vecteur** (une liste de 384 nombres) par un modèle de langage léger comme `all-MiniLM-L6-v2` (80 Mo, s'exécute en ~2ms par document sur CPU). Ce vecteur capture le sens — deux phrases qui veulent dire la même chose auront des vecteurs très proches.
- Au moment de la recherche, la requête utilisateur est elle aussi convertie en vecteur.
- On calcule la **similarité cosinus** entre le vecteur de la requête et les vecteurs de tous les documents. Les documents avec la similarité la plus élevée sont retournés.
- **Recherche hybride** : on combine le score TF-IDF (pertinence par mots-clés exacts) et le score sémantique (pertinence par sens) via RRF (Reciprocal Rank Fusion) — les documents bien classés par les deux méthodes remontent en premier.
- **Stockage** : les vecteurs sont stockés avec FAISS (Facebook AI Similarity Search), une bibliothèque optimisée qui permet de chercher parmi des millions de vecteurs en quelques millisecondes.
- **Indexation** : les embeddings sont générés une fois à l'indexation du fichier (pas à chaque recherche). Un fichier modifié voit son embedding regénéré automatiquement par le watcher.
- **Pourquoi c'est important :** La recherche par mots-clés échoue dans ~30% des cas où l'utilisateur ne se souvient pas des mots exacts utilisés dans ses notes. La recherche sémantique résout ce problème. C'est particulièrement utile pour les gros vaults (500+ notes) où on ne peut pas tout parcourir manuellement.
- **Sous-tâches :**
- [ ] Génération d'embeddings : modèle `all-MiniLM-L6-v2` via `sentence-transformers` (Python) ou appel API externe
- [ ] Stockage : index vectoriel avec `numpy` + `faiss` (ou `usearch` pour performance)
- [ ] Indexation : embedding par chunk de 512 tokens avec recouvrement
- [ ] Recherche hybride : combinaison TF-IDF + similarité cosinus (RRF — Reciprocal Rank Fusion)
- [ ] UI : toggle « Recherche sémantique » dans la barre de recherche
- [ ] UI : score de similarité dans les résultats
### 73. Synchronisation multi-appareils — Obsidian Sync compatible
- **Effort :** 6-8 jours | **Impact :** 🟢
@@ -116,6 +95,7 @@
| 79 | Assistant IA — Outils (function calling) & serveur MCP | 2.3.0 | [features/ai-tools-mcp.md](./features/ai-tools-mcp.md) |
| 80 | Assistant IA — Rendu Markdown, liens fichiers/paths & sessions | 2.3.0 | [features/ai-assistant-ux.md](./features/ai-assistant-ux.md) |
| 69 | Éditeur mobile natif — Interface tactile optimisée | 2.3.0 | [features/mobile-editor.md](./features/mobile-editor.md) |
| 70 | Recherche sémantique — Embeddings vectoriels (hybride TF-IDF + RRF) | 2.3.0 | [features/semantic-search.md](./features/semantic-search.md) |
| 77 | Application Desktop native — Tauri | 🔵 en cours | [features/desktop-tauri.md](./features/desktop-tauri.md) |
---
@@ -124,10 +104,10 @@
| Priorité | Items | Effort total estimé |
|---|---|---|
| ✅ Complété | #1 → #59, #61–72, #74–76, #78–80 | ~99 jours réalisés |
| ✅ Complété | #1 → #59, #61–72, #74–76, #78–80 | ~103 jours réalisés |
| 🔵 P2 restant | #77 Desktop : signature code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E **manuels** | ~1-2 jours |
| ⚪ P4 restant | #70 Sémantique (4-5j) · #73 Sync (6-8j) | 10-13 jours |
| **Total restant** | **3 items + finitions** | **~11-15 jours** |
| ⚪ P4 restant | #73 Sync (6-8j) | 6-8 jours |
| **Total restant** | **2 items + finitions** | **~7-10 jours** |
---
+128
View File
@@ -0,0 +1,128 @@
# #70 - Recherche sémantique - Embeddings vectoriels
> **Statut :** ✅ Terminé — 100 % implémenté + 27 tests backend + 4 tests frontend (2026-09-12)
> **Effort :** 4-5 jours (réalisé) | **Impact :** 🟢
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog](../../CHANGELOG.md)
- **Fichiers clés :**
- `backend/semantic_search.py` - chunking, providers d'embeddings, `VectorStore`, `SemanticIndex`, RRF, hook incrémental
- `backend/search.py` - fusion RRF dans `advanced_search(..., semantic=True)` + `_fuse_semantic_results()`
- `backend/services/search.py` - paramètre `semantic` du service
- `backend/main.py` - paramètre `semantic` de `/api/search/advanced`, schémas, `init_semantic_index()` au démarrage
- `frontend/js/search.js` - toggle `#rb-semantic`, score de similarité, raccourci `Alt+S`
- `frontend/js/state.js` - `semanticSearch`, `semanticAvailable`
- `frontend/index.html` - bouton `#rb-semantic` dans la barre de résultats
- `frontend/locales/fr.json`, `frontend/locales/en.json` - clés `search.semantic_*`
- `backend/requirements-semantic.txt` - dépendances **optionnelles** (sentence-transformers, numpy, faiss-cpu)
- `tests/test_semantic_search.py` - 27 tests backend
- `tests/frontend/semantic-search.test.mjs` - 4 tests JSDOM
- **Description :** la recherche TF-IDF historique ne trouve que les documents contenant
**exactement** les mots tapés. La recherche sémantique comprend le **sens** de la requête et
retrouve des documents pertinents même formulés différemment (« comment sauvegarder mes
données » remonte aussi « Stratégie de backup automatique »). Les deux classements sont
fusionnés via **RRF** (Reciprocal Rank Fusion), ce qui combine précision lexicale et rappel
sémantique.
---
## Architecture
```
┌───────────────────────────────┐
indexer (watcher) │ on_index_change(action, …) │
─────────────────► │ SemanticIndex.add/remove │
└──────────────┬────────────────┘
│ chunks (512 mots, recouvrement 64)
▼
┌───────────────────────────────┐
│ EmbeddingProvider │
│ sentence-transformers │ API │
│ │ hash (fallback sans dep) │
└──────────────┬────────────────┘
│ vecteurs 384 dim, L2-normalisés
▼
┌───────────────────────────────┐
│ VectorStore (numpy / faiss │
│ / pur Python) — cosinus │
└──────────────┬────────────────┘
requête ──► embed ──► similarité ──┘
│
TF-IDF ranking ──────────► RRF ◄──┘ ──► résultats fusionnés (semantic_score)
```
## Backend - `backend/semantic_search.py`
- **Chunking** : `chunk_text(text, chunk_tokens=512, overlap=64)` découpe chaque document en
fenêtres glissantes de 512 mots avec 64 mots de recouvrement, pour ne pas perdre le contexte
aux frontières.
- **Providers d'embeddings** (`EmbeddingProvider`) :
- `SentenceTransformerProvider` — modèle local `all-MiniLM-L6-v2` (384 dim), chargé
paresseusement. Actif si `sentence-transformers` est installé.
- `RemoteEmbeddingProvider` — endpoint `/embeddings` compatible OpenAI
(`OBSIGATE_EMBEDDING_API_KEY`, `OBSIGATE_EMBEDDING_BASE_URL`, `OBSIGATE_EMBEDDING_MODEL`).
- `HashEmbeddingProvider` — **repli sans aucune dépendance** : hachage signé déterministe des
unigrammes, bigrammes et trigrammes de caractères (hashing trick) + normalisation L2. Il
capture le vocabulaire partagé et les variantes morphologiques, mais pas les synonymes.
- Sélection via `OBSIGATE_EMBEDDING_PROVIDER=auto|local|remote|hash` (défaut `auto`).
- **`VectorStore`** : stocke les vecteurs par chunk. Accélération optionnelle **numpy** (produit
matriciel) puis **faiss** (`IndexFlatIP`) ; sinon cosinus pur Python. Les vecteurs étant
L2-normalisés, le produit scalaire vaut la similarité cosinus.
- **`SemanticIndex`** : singleton `get_semantic_index()`. `rebuild()` construit tout depuis
`backend.indexer.index` ; `add_document()` / `remove_document()` mettent à jour un document à
chaud (appelés par le hook `on_index_change`). `search()` regroupe les chunks par document en
gardant la meilleure similarité et filtre par vault.
- **RRF** : `rrf_fuse(rankings, k=60)` — `score = Σ 1/(k + rang)`.
## Backend - intégration recherche
`advanced_search(..., semantic=True)` :
1. Le classement TF-IDF est calculé comme avant.
2. `_fuse_semantic_results()` récupère les hits sémantiques, les restreint aux mêmes filtres
(vault, tags, `title:`, `path:`, `ext:`, dates, taille, include/exclude) et fusionne les deux
classements par RRF. Les documents trouvés uniquement par le sémantique sont matérialisés
depuis l'index inversé (snippet brut, sans `<mark>`).
3. Chaque résultat porte `semantic_score` (similarité cosinus, `0.0` si absent) et la réponse
expose `semantic_available`.
Le mode sémantique est ignoré si `regex=True` (notion purement lexicale).
## API
`GET /api/search/advanced?...&semantic=true` ajoute :
- `results[].semantic_score` (float) ;
- `semantic_available` (bool) : l'index sémantique est prêt.
## Frontend
- Bouton `~` (`#rb-semantic`) dans la barre de résultats, actif seulement quand
`semantic_available` est vrai (sinon `disabled`).
- Raccourci clavier **Alt+S** ; l'état est conservé dans `state.semanticSearch`.
- Le badge de score affiche `score: … · sim: …` quand un score sémantique existe.
- Clés i18n `search.semantic_title`, `search.semantic_score`, `search.semantic_unavailable`.
## Configuration
| Variable | Défaut | Rôle |
|---|---|---|
| `OBSIGATE_EMBEDDING_PROVIDER` | `auto` | `auto`, `local`, `remote` ou `hash` |
| `OBSIGATE_EMBEDDING_MODEL` | `all-MiniLM-L6-v2` | Modèle local ou nom de modèle distant |
| `OBSIGATE_EMBEDDING_API_KEY` | — | Clé du provider distant |
| `OBSIGATE_EMBEDDING_BASE_URL` | — | URL de base du provider distant |
| `OBSIGATE_EMBEDDING_DIM` | `384` | Dimension attendue pour le provider distant |
## Dépendances optionnelles
`pip install -r backend/requirements-semantic.txt` (sentence-transformers + numpy + faiss-cpu).
**Sans installation, la fonctionnalité reste opérationnelle** grâce au provider `hash` et au
stockage pur Python : le CI n'installe que `backend/requirements.txt`.
## Tests
- `tests/test_semantic_search.py` (27) : chunking, provider hash (déterminisme, normalisation,
similarité), `VectorStore`, RRF, `SemanticIndex`, hook incrémental, `advanced_search`
sémantique, endpoint API.
- `tests/frontend/semantic-search.test.mjs` (4) : toggle désactivé tant que l'index n'est pas
prêt, bascule état + classe active, no-op si indisponible.