feat(search): recherche semantique - embeddings vectoriels + hybride RRF (#70)
This commit is contained in:
+5
-25
@@ -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** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user