Files

7.4 KiB

#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 · Changelog

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