# V6.9.0 — Recherche sémantique + Ask AI > **Statut** : ✅ Livré en v6.9.0 (2026-09-28) — `app/services/semantic_search.py`, `app/routers/search_ai.py`, migration 25, 24 tests verts · **Roadmap** : `ROADMAP.md § v6.9.0` > Note d'implémentation : table générique `semantic_embeddings(resource_type, resource_id, …)` au lieu de `page_embeddings(page_id, …)` — couvre aussi les collections ; encodeur hashed-TF `hash-256` (pas d'appel `/embeddings` externe) ; pièces jointes et onglets palette en follow-up. > **Référence Notion** : Enterprise Search (Notion + Slack/Jira/Drive) + AI Q&A avec citations. > **Existant** : FTS5 (`services/search.py`, `GET /api/search`, `GET /api/v2/search`), palette `Ctrl+K`, `AgentEngine` (ReAct 12 tours), `PermissionManager`, `llm_config` + `LLMClient` (9 providers). --- ## 1. Vision `question → top-k chunks autorisés → réponse avec [[fdpage:ID]] cliquables`. Hybride lexical + vectoriel (RRF), jamais de fuite ACL (filtrage avant prompt). Hors scope : index temps réel < 1s, connecteurs Slack/Jira (v7.x si besoin forge d'abord). ## 2. Indexation ### 2.1 Schéma (migration 25) ```sql CREATE TABLE page_embeddings ( page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE, chunk_id INTEGER NOT NULL, -- index du chunk (0..N) chunk_text TEXT NOT NULL, embedding BLOB NOT NULL, -- float32 serialisé model TEXT NOT NULL DEFAULT 'local-tfidf', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (page_id, chunk_id) ); CREATE TABLE search_index_state (page_id INTEGER PRIMARY KEY, indexed_at TIMESTAMP); ALTER TABLE pages ADD COLUMN search_excluded BOOLEAN NOT NULL DEFAULT 0; ``` - Chunking : blocs → texte brut (~500 tokens, overlap 50), ignore `embed`/images (légende seule). - Providers : `openai-compatible /embeddings` via `LLMClient` si clé user/workspace, sinon **TF-IDF local** (zéro dépendance, FR+EN stopwords) — même interface `embed(texts) -> vectors`. - Job : scheduler 5 min, batch 50 (`updated_at > indexed_at`), `DELETE` embeddings si page trashée/exclue. ### 2.2 Contenus indexés Pages `blocks` + `markdown`/`file` (résolution déjà `export.py`), pièces jointes texte (pdf via `pypdf`, docx via `python-docx`, txt/md — cap 1 MB/fichier), titres de lignes DB (sans valeurs privées si `property_permissions` restreint → chunk « titre seul »), issues Gitea/GitHub liées (titre + labels, pas de body privé sans grant). ## 3. Recherche hybride `GET /api/v2/search/hybrid?q=&workspace_id=&type=&limit=20` : 1. FTS5 score (existants) ; 2. cosine top-50 ; 3. fusion RRF `k=60` ; 4. filtre ACL (`can_view_page`/`can_view_collection`) ; 5. `X-Total-Count`. Filtres : `verified:1`, `type:page|collection|file`, `after:YYYY-MM-DD`. Palette `Ctrl+K` : onglets `Pages / Fichiers / ✨ Réponses IA`. ## 4. Ask AI `POST /api/v2/search/ask {question, workspace_id}` → `{answer_markdown, citations: [{page_id, title}]}` : - top-k=8 chunks autorisés → prompt `system: réponds en français, cite [[fdpage:ID]]` → `LLMClient.chat()` (clé user, sinon mock déterministe « hors-ligne » comme `ai_writing.py`). - Budget : 4k tokens contexte, timeout 60s, cache `(hash(question)+workspace)` 10 min. - Citations résolues via `wiki/titles` (renommage propagé, « Deleted page » gérée). ## 5. UI / perfs / sécu - Badge `✨` + temps de réponse, feedback 👍/👎 (`ask_feedback` log, futur fine-tune). - ACL avant LLM (jamais de chunk interdit dans le prompt) ; `search_excluded` respecté partout ; audit `api_audit_log`. - Perfs : embeddings lazy (pas au save, job fond), cosine en numpy si dispo sinon pur Python (DB < 10k pages OK). ## 6. Tests (`tests/test_v69_search_ask.py`, ~20) Index chunks, hybride RRF ordre, ACL (page restricted exclue), `verified` filtre, exclusion flag, pièces jointes cap, ask citations valides, ask sans clé → fallback, cache hit, rate-limit, `X-Total-Count`. ## 7. Rollout 1. `embed()` + TF-IDF + job. 2. `/hybrid` + palette. 3. `/ask` + cache + feedback. 4. Docs `/help` + OpenAPI.