SPB Git forge

spb/trouve-ka

Public ★ Pinned

Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com

40commits 1branches 0releases
9.2 MBsize
maindefault branch
23 days agolast push
Python 51.9% TypeScript 32.7% JavaScript 4.8% CSS 4.1% Shell 2.9% HTML 1.8% SQL 1.5%

docs: README v3 — chiffres live vérifiés, pastilles dynamiques, sections complètes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 24, 2026) parent 83c2a1d

1 changed file +185 −67

modified README.md +185 −67
@@ -1,8 +1,8 @@
1 1 <p align="center">
2 − <a href="https://www.trouve-ka.com"><img src="https://www.trouve-ka.com/og.png" width="760" alt="Trouve·Ka — Le moteur de recherche du web québécois"></a>
2 + <a href="https://www.trouve-ka.com"><img src="https://www.trouve-ka.com/og.png" width="760" alt="Trouve·Ka — Le moteur de recherche du Groupe KA"></a>
3 3 </p>
4 4 <h1 align="center">Trouve·Ka</h1>
5 −<p align="center"><b>Le moteur de recherche du web québécois</b></p>
5 +<p align="center"><b>Le moteur de recherche du Groupe KA — bâti au Québec, de zéro</b></p>
6 6
7 7 <div align="center">
8 8
@@ -11,25 +11,56 @@
11 11 [![PDF](https://img.shields.io/badge/guide-PDF-1c7ed6?style=flat-square)](https://www.trouve-ka.com/doc/trouve-ka-documentation.pdf)
12 12 ![Nœud](https://img.shields.io/badge/n%C5%93ud-M2M32-1c7ed6?style=flat-square)
13 13 ![Port](https://img.shields.io/badge/port-3000-1c7ed6?style=flat-square)
14 −![PM2](https://img.shields.io/badge/process-PM2-2b037a?style=flat-square)
15 −![Docker](https://img.shields.io/badge/Docker-Postgres%20%2B%20Redis-2496ED?style=flat-square&logo=docker)
14 +![PM2](https://img.shields.io/badge/PM2-9_processus-2b037a?style=flat-square)
15 +![launchd](https://img.shields.io/badge/launchd-ngrok%20%C2%B7%20watchdog%20%C2%B7%20backups-1c7ed6?style=flat-square)
16 +
17 +[![Pages indexées](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.trouve-ka.com%2Fapi%2Fstatus&query=%24.pages_indexed&label=pages%20index%C3%A9es&color=1c7ed6&style=flat-square)](https://www.trouve-ka.com/status)
18 +[![Sites KA indexés](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.trouve-ka.com%2Fapi%2Fstatus&query=%24.domains_count&label=sites%20KA%20index%C3%A9s&color=1c7ed6&style=flat-square)](https://www.trouve-ka.com/status)
19 +[![Indexées / heure](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.trouve-ka.com%2Fapi%2Fstatus&query=%24.indexed_last_hour&label=index%C3%A9es%20%2F%20heure&color=1c7ed6&style=flat-square)](https://www.trouve-ka.com/status)
20 +[![Fetchées / heure](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.trouve-ka.com%2Fapi%2Fstatus&query=%24.fetched_last_hour&label=fetch%C3%A9es%20%2F%20heure&color=1c7ed6&style=flat-square)](https://www.trouve-ka.com/status)
21 +[![Frontier](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.trouve-ka.com%2Fapi%2Fstatus&query=%24.frontier_pending&label=frontier%20(URL)&color=555555&style=flat-square)](https://www.trouve-ka.com/status)
22 +[![Crawler](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fwww.trouve-ka.com%2Fapi%2Fstatus&query=%24.crawler_state&label=crawler&color=2ea44f&style=flat-square)](https://www.trouve-ka.com/status)
23 +![Couverture sémantique](https://img.shields.io/badge/couverture%20s%C3%A9mantique-100%20%25-1c7ed6?style=flat-square)
24 +
16 25 ![Next.js](https://img.shields.io/badge/Next.js-15-000000?style=flat-square&logo=nextdotjs)
17 −![Python](https://img.shields.io/badge/Python-3.12%20%C2%B7%20FastAPI-3776ab?style=flat-square&logo=python&logoColor=white)
26 +![Python](https://img.shields.io/badge/Python-3.12-3776ab?style=flat-square&logo=python&logoColor=white)
27 +![FastAPI](https://img.shields.io/badge/FastAPI-uvicorn-009688?style=flat-square&logo=fastapi&logoColor=white)
18 28 ![OpenSearch](https://img.shields.io/badge/OpenSearch-2.17-005eb8?style=flat-square&logo=opensearch&logoColor=white)
29 +![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-4169e1?style=flat-square&logo=postgresql&logoColor=white)
30 +![Redis](https://img.shields.io/badge/Redis-7-dc382d?style=flat-square&logo=redis&logoColor=white)
31 +![Docker](https://img.shields.io/badge/Docker-Postgres%20%C2%B7%20Redis%20%C2%B7%20OpenSearch-2496ED?style=flat-square&logo=docker&logoColor=white)
19 32 ![Groupe KA](https://img.shields.io/badge/Groupe-KA-b7f000?style=flat-square)
33 +![spbgit](https://img.shields.io/badge/remote--first-spbgit-1c7ed6?style=flat-square&logo=git&logoColor=white)
20 34
21 35 </div>
22 36
23 −**Trouve·Ka** est un **vrai moteur de recherche québécois** — pas un métamoteur : **aucune dépendance à Google, Bing ou Brave** pour les résultats. Le crawler découvre le web québécois à partir de **64 seeds à forte autorité** (gouvernement, municipalités, universités, médias), suit les liens sortants, juge la **pertinence québécoise** de chaque page (`page_quebec_score` et `domain_quebec_score`) et indexe **immédiatement** : une page fetchée est **cherchable en ~2–4 secondes**, pendant que le frontier continue de grandir. L'enrichissement (autorité, entités, embeddings) arrive après, en asynchrone, sans jamais bloquer. C'est aussi **le moteur de recherche du Groupe KA** : les **14 sites de l'écosystème** (logements, propriétés, emplois, restos, événements, produits, véhicules, créateurs…) sont indexés en continu, avec **couverture sémantique de 100 %** de l'index.
37 +> Les pastilles de la deuxième rangée sont **dynamiques** : elles interrogent [`/api/status`](https://www.trouve-ka.com/api/status) en direct — les chiffres que tu vois là-haut sont ceux du moteur en ce moment même.
38 +
39 +**Trouve·Ka** est un **vrai moteur de recherche** — pas un métamoteur : **aucune dépendance à Google, Bing ou Brave** pour les résultats. Crawler, frontier, parseur, classification, index, ranking, sémantique : tout est maison. Né « moteur du web québécois ouvert » (64 seeds à forte autorité — gouvernement, municipalités, universités, médias — 7 898 domaines découverts), il a **pivoté le 2026-08-23** pour devenir **le moteur de recherche du Groupe KA** : il indexe en continu les **14 sites de l'écosystème** (logements, propriétés, emplois, restos, événements, produits, véhicules, créateurs, estimations…), alimenté par leurs **sitemaps ingérés quotidiennement** — le seul frontier dépasse les **4,6 M d'URL** en attente. Chaque page fetchée est jugée (`page_quebec_score`, `domain_quebec_score`) et indexée **immédiatement** : **cherchable en ~2–4 secondes**. L'enrichissement (autorité, entités, embeddings, reranking) arrive après, en asynchrone, sans jamais bloquer — avec une **couverture sémantique de 100 %** de l'index.
24 40
25 41 > **Crawl en continu. Indexe immédiatement. Recherche immédiatement. Améliore en asynchrone.**
26 42
27 43 ```
28 −Crawler → Frontier → Fetcher → Parser → Classification Québec
29 −→ Déduplication → Indexer → Index → Ranking → API → Web App
44 +Sitemaps KA + Seeds → Frontier → Fetcher → Parser → Classification Québec
45 +→ Déduplication → Indexer → Index → Ranking (+ sémantique, rerank) → API → Web App
30 46 ```
31 47
32 −En production : **https://www.trouve-ka.com** — **177 000+ pages indexées** (181 369 constatées en direct sur [/status](https://www.trouve-ka.com/status) le 24 août 2026 — la croissance de l'index fait partie du produit). Détails techniques dans [docs/architecture.md](docs/architecture.md) (diagrammes Mermaid) et [docs/decisions.md](docs/decisions.md) ; guide utilisateur illustré sur [/doc](https://www.trouve-ka.com/doc/index.html).
48 +En production : **https://www.trouve-ka.com** — **302 000+ pages indexées** (302 017 constatées sur [/api/status](https://www.trouve-ka.com/api/status) le 24 août 2026 à 14 h 47, contre 181 369 le matin même : **+120 000 pages dans la journée** — la croissance de l'index fait partie du produit, la pastille dynamique ci-dessus fait foi). Détails techniques dans [docs/architecture.md](docs/architecture.md) (diagrammes Mermaid) et [docs/decisions.md](docs/decisions.md) ; guide utilisateur illustré sur [/doc](https://www.trouve-ka.com/doc/index.html).
49 +
50 +## Chiffres live (instantané du 2026-08-24)
51 +
52 +| Métrique | Valeur constatée | Source |
53 +|---|---|---|
54 +| Pages indexées | **302 017** (et ça monte — voir pastille dynamique) | `/api/status` |
55 +| Sites du Groupe KA indexés | **14** | `/api/status` |
56 +| Pages indexées, dernière heure | **8 482** | `/api/status` |
57 +| Pages fetchées, dernière heure | **13 287** | `/api/status` |
58 +| Frontier (URL en attente) | **4 646 668** | `/api/status` |
59 +| Couverture sémantique | **100 %** | `/api/status` |
60 +| Pages crawlées — 30 jours | **357 209** | `/api/stats/dashboard` |
61 +| Erreurs de crawl — 30 jours | **641** (~0,2 %) | `/api/stats/dashboard` |
62 +| Latence de recherche (sémantique + rerank actifs) | **258–425 ms** sur 8 requêtes tests | `/api/search` (`took_ms`) |
63 +| Workers de crawl | **9**, répartis sur **5 nœuds** | `pm2 ls` + satellites |
33 64
34 65 ## Visite guidée
35 66
@@ -41,12 +72,12 @@ Toutes les captures sont trackées dans le repo (`docs/screenshots/` et `apps/we
41 72 <td align="center"><img src="docs/screenshots/trouve-ka-mobile.png" width="240"><br><sub><b>Accueil mobile — même expérience, pastille live du compteur d'index</b></sub></td>
42 73 </tr>
43 74 <tr>
44 − <td align="center"><img src="apps/web/public/doc/img/etape2.png" width="420"><br><sub><b>Résultats « cabane à sucre » — 693 résultats en 269 ms, recherche sémantique active, filtres par univers KA / langue / fraîcheur</b></sub></td>
75 + <td align="center"><img src="apps/web/public/doc/img/etape2.png" width="420"><br><sub><b>Résultats « cabane à sucre » — 693 résultats en 269 ms à la capture (1 131 résultats au 2026-08-24), recherche sémantique active, filtres par univers KA / langue / fraîcheur</b></sub></td>
45 76 <td align="center"><img src="apps/web/public/doc/img/etape3.png" width="420"><br><sub><b>Onglet Images — 692 résultats en 74 ms, galerie hotlink avec attribution du domaine source</b></sub></td>
46 77 </tr>
47 78 <tr>
48 − <td align="center"><img src="apps/web/public/doc/img/etape4.png" width="420"><br><sub><b>État du moteur (/status) — 177 301 pages indexées, couverture sémantique 100 %, 14 sites du Groupe KA, 4,58 M d'URL au frontier</b></sub></td>
49 − <td align="center"><img src="docs/screenshots/resultats.png" width="420"><br><sub><b>« plombier gatineau » — 4 278 résultats en 21 ms, badges Québec, filtres Tout / Images / Actualités / Gouvernement / FR / EN</b></sub></td>
79 + <td align="center"><img src="apps/web/public/doc/img/etape4.png" width="420"><br><sub><b>État du moteur (/status) — 177 301 pages indexées à la capture (302 000+ au 2026-08-24), couverture sémantique 100 %, 14 sites du Groupe KA, 4,6 M d'URL au frontier</b></sub></td>
80 + <td align="center"><img src="docs/screenshots/resultats.png" width="420"><br><sub><b>« plombier gatineau » — 4 278 résultats en 21 ms, badges Québec, filtres Tout / Images / Actualités / Gouvernement / FR / EN (phase « web québécois ouvert »)</b></sub></td>
50 81 </tr>
51 82 <tr>
52 83 <td align="center"><img src="docs/screenshots/images.png" width="420"><br><sub><b>Onglet Images sur « montréal » — 1 554 résultats en 29 ms (phase crawl du web québécois ouvert)</b></sub></td>
@@ -59,70 +90,136 @@ Toutes les captures sont trackées dans le repo (`docs/screenshots/` et `apps/we
59 90
60 91 ## Fonctionnalités
61 92
62 −- **Recherche Québec-first** : **177 000+ pages indexées** (181 369 constatées live), **7 900+ domaines québécois** découverts à partir de 64 seeds, **14 sites du Groupe KA** indexés en continu, **latence de recherche 20–95 ms** en lexical (mesurée en prod ; jusqu'à ~270 ms quand la réécriture sémantique s'active).
93 +- **Moteur du Groupe KA** : **302 000+ pages indexées** (302 017 constatées live au 2026-08-24), **14 sites de l'écosystème** indexés en continu, alimentés par **ingestion quotidienne des sitemaps** (`tk-sitemaps` — vrai-prix déclare à lui seul ~3,75 M d'URL sur 84 chunks, traités en streaming) ; **4,6 M d'URL** au frontier.
63 94 - **Indexation incrémentale** : fetch → parse → score → index inline (`refresh_interval: 1s` côté OpenSearch) → cherchable en secondes. Détection de changement par hash de contenu + ETag/If-Modified-Since, recrawl adaptatif (inchangé → intervalle ×2, volatil → ÷2).
64 −- **Détection Québec déterministe** : TLD (.qc.ca, .quebec), gazetteer de toponymes, codes postaux G/H/J, indicatifs (418/514/438/…), organisations connues (Hydro-Québec, RAMQ, UQAM…), JSON-LD, langue française — aucun LLM dans le chemin chaud.
95 +- **Détection Québec déterministe** : TLD (.qc.ca, .quebec), gazetteer de toponymes, codes postaux G/H/J, indicatifs (418/514/438/…), organisations connues (Hydro-Québec, RAMQ, UQAM…), JSON-LD, langue française — aucun LLM dans le chemin chaud. Héritée de la phase « web ouvert », toujours active (`ka_only=False` dans la config restaure le comportement web québécois ouvert — le pivot est réversible).
65 96 - **Ranking bilingue** : `function_score` OpenSearch — BM25 FR/EN avec synonymes bilingues (thermopompe ↔ heat pump) + scores Québec + autorité de domaine (inlinks pondérés) + fraîcheur + boost de localité (« plombier Gatineau » → documents avec preuve géographique).
66 −- **Recherche sémantique** : embeddings sur 100 % de l'index — « heat pump » trouve « thermopompe », pas seulement par mots-clés ; la couverture progresse en arrière-plan sans bloquer le lexical.
97 +- **Recherche sémantique + reranking** : embeddings sur **100 % de l'index** — « heat pump » trouve « thermopompe », pas seulement par mots-clés — et un **reranker** réordonne les meilleurs résultats (service dédié sur le nœud m4mc). Latence mesurée au 2026-08-24 : **258–425 ms** avec sémantique + rerank actifs sur toutes les requêtes tests ; le chemin lexical pur descendait à **20–95 ms** (captures de la phase web ouvert).
67 98 - **Crawler poli et assumé** : UA identifié `TrouveKABot`, page publique [/trouveka-bot](https://www.trouve-ka.com/trouveka-bot), robots.txt respecté (Protego, cache 24 h), verrou de politesse Redis par hôte (défaut 2 s, `Crawl-delay` honoré), garde SSRF (IP privées/loopback/métadonnées cloud bloquées, revalidée à chaque redirection), détection de pièges de crawl (session IDs, calendriers infinis, facettes explosives).
68 99 - **Onglet Images** : galerie des pages avec image représentative (hotlink + attribution, jamais de crawl d'images).
69 −- **Métriques publiques réelles** : `/status` et `/stats` (tableau de bord + rapports PDF Groupe KA) — aucun compteur simulé, c'est une règle du projet. Les chiffres se rafraîchissent toutes les 10 secondes.
70 −- **Crawl distribué multi-nœuds** : workers satellites sur M2M32b et M2M32c, coordonnés sans orchestrateur via le frontier Postgres (`FOR UPDATE SKIP LOCKED`) et les verrous Redis, communication par le LAN interne 192.168.2.x.
71 −- **SSO KA ID** + favoris « Mon univers Ka », design Groupe KA et widget **KA Agent** (bulle de chat IA).
100 +- **Métriques publiques réelles** : `/status` et `/stats` (tableau de bord + rapports PDF Groupe KA personnalisables) — aucun compteur simulé, c'est une règle du projet. Les chiffres se rafraîchissent toutes les 10 secondes — et les pastilles de ce README interrogent la même API.
101 +- **Crawl distribué multi-nœuds auto-guéri** : **9 workers sur 5 nœuds** (2 sur le hub M2M32, 3 sur M2M32c, 2 sur M4BP48, 1 sur M1M32, 1 sur m4ma), coordonnés **sans orchestrateur** via le frontier Postgres (`FOR UPDATE SKIP LOCKED`) et les verrous Redis. Tout le trafic inter-nœuds passe par des **tunnels SSH sur 127.0.0.1** (contournement du filtre « réseau local » de macOS 26, qui bloque les binaires non signés). Watchdog toutes les 2 min, passe de flotte ~30 min, survie au reboot prouvée (API tuée → ressuscitée en 40 s ; reboot complet de m4ma → tunnels + worker revenus seuls).
102 +- **SSO KA ID** + favoris « Mon univers Ka », design Groupe KA, SEO complet (canonical, JSON-LD WebSite/SearchAction/Organization, sitemap) et widget **KA Agent** (bulle de chat IA déplaçable).
72 103
73 104 ## Pipeline complet
74 105
75 106 Le chemin d'une page, de sa découverte à sa présence dans les résultats :
76 107
77 −1. **Découverte** — le frontier (Postgres) part de 64 seeds à forte autorité et grandit avec chaque lien sortant découvert (4,5 M+ d'URL en attente en prod). Les workers se servent sans orchestrateur via `FOR UPDATE SKIP LOCKED`.
108 +1. **Découverte** — le frontier (Postgres) est alimenté par les **14 seeds** (les pages d'accueil des sites KA, `scripts/bootstrap-seeds/seeds.txt`) et surtout par l'**ingestion quotidienne des sitemaps** des sites du Groupe KA (`scripts/ka-sitemaps/ingest.py`, process PM2 `tk-sitemaps`) : découverte via robots.txt (`Sitemap:`), suivi des index de sitemaps, traitement en streaming chunk par chunk, priorité aux pages peu profondes (accueil, villes, catégories) avant les fiches. 4,6 M+ d'URL en attente en prod. Les workers se servent sans orchestrateur via `FOR UPDATE SKIP LOCKED`.
78 109 2. **Fetch poli** — `TrouveKABot` prend un verrou de politesse Redis par hôte (2 s par défaut, `Crawl-delay` honoré), vérifie robots.txt (Protego, cache 24 h) et la garde SSRF, puis télécharge avec ETag/If-Modified-Since.
79 −3. **Parsing** — extraction du contenu principal, du titre, des métadonnées, du JSON-LD, des liens sortants et d'une image représentative.
110 +3. **Parsing** — extraction du contenu principal, du titre, des métadonnées, du JSON-LD, des liens sortants et d'une image représentative (selectolax).
80 111 4. **Classification Québec** — calcul déterministe de `page_quebec_score` et `domain_quebec_score` (TLD, toponymes, codes postaux, indicatifs, organisations, langue) — aucun LLM dans le chemin chaud.
81 112 5. **Déduplication + indexation inline** — hash de contenu, puis indexation immédiate dans **OpenSearch 2.17** (`refresh_interval: 1s`) : la page est **cherchable en ~2–4 s**.
82 113 6. **Enrichissement asynchrone** — via **Redis Streams** : autorité de domaine (inlinks pondérés), entités, embeddings sémantiques. Ne bloque jamais l'indexation ; en panne, le lexical continue.
83 114 7. **Recrawl adaptatif** — le scheduler recale l'intervalle de revisite : contenu inchangé → intervalle ×2, contenu volatil → ÷2.
84 −8. **Ranking** — `function_score` : BM25 FR/EN + synonymes bilingues + scores Québec + autorité + fraîcheur + boost de localité, avec réécriture sémantique quand elle aide.
115 +8. **Ranking** — `function_score` : BM25 FR/EN + synonymes bilingues + scores Québec + autorité + fraîcheur + boost de localité, avec réécriture sémantique et **reranking** quand ils aident.
85 116
86 117 ## API
87 118
88 −L'API FastAPI (`trouveka.api`) est servie derrière le proxy Next.js (`/api/*`) — un seul port exposé (3000).
119 +L'API FastAPI (`trouveka.api`) est servie derrière le proxy Next.js (`/api/*`) — un seul port exposé (3000). Toutes les routes ci-dessous sont celles du code (`@app.get/post`), vérifiées au 2026-08-24 :
89 120
90 121 | Endpoint | Méthode | Rôle |
91 122 |---|---|---|
92 −| `/api/search` | GET | Recherche principale (requête, filtres univers/langue/fraîcheur, onglet images) |
93 −| `/api/suggest` | GET | Suggestions de saisie |
94 −| `/api/status` | GET | État du moteur en JSON : pages indexées, débits, frontier, erreurs, couverture sémantique |
95 −| `/api/live` | GET | Flux « en ce moment » (page en cours de crawl) |
123 +| `/api/search` | GET | Recherche principale — requête, filtres univers KA/langue/fraîcheur, onglet images ; renvoie `total`, `took_ms`, `semantic`, `reranked`, `related` |
124 +| `/api/suggest` | GET | Suggestions de saisie (autocomplete) |
125 +| `/api/status` | GET | État du moteur en JSON : `pages_indexed`, `domains_count`, `indexed_last_hour`, `fetched_last_hour`, `errors_last_hour`, `frontier_pending`, `frontier_in_progress`, `crawler_state`, `search_ok`, `embedding_coverage` |
126 +| `/api/live` | GET | Flux « en ce moment » — la page en cours de crawl (url, domaine, issue) |
96 127 | `/api/submit` | POST | Soumission publique d'une URL au crawl ([/soumettre](https://www.trouve-ka.com/soumettre)) |
97 −| `/api/stats/dashboard` · `/catalog` · `/report` | GET/POST | Tableau de bord `/stats` + rapports PDF Groupe KA |
98 −| `/api/health` | GET | Sonde de santé |
99 −| `/api/admin/*` (overview, recent, frontier, zero-results, pause, resume, recrawl, seeds, domains/block) | GET/POST | Endpoints d'exploitation, protégés par `X-Admin-Token` |
100 −| `/embed` · `/rerank` | POST | Service d'embeddings interne (enrichissement sémantique) |
128 +| `/api/health` | GET | Sonde de santé (`{"ok": true, "search_ok": true}`) |
129 +| `/api/stats/dashboard` | GET | KPI, jauges, séries 30 jours du tableau de bord `/stats` |
130 +| `/api/stats/catalog` | GET | Catalogue des blocs disponibles pour les rapports |
131 +| `/api/stats/report` | GET | Rapport PDF Groupe KA standard |
132 +| `/api/stats/report/custom` | POST | Rapport PDF personnalisé (ReportBuilder — blocs et rendus au choix) |
133 +| `/api/admin/overview` · `/recent` · `/frontier` · `/zero-results` | GET | Exploitation : vue d'ensemble, derniers crawls, état du frontier, requêtes sans résultat — protégés par `X-Admin-Token` |
134 +| `/api/admin/pause` · `/resume` · `/recrawl` · `/seeds` · `/domains/block` | POST | Exploitation : pause/reprise du crawl, recrawl forcé, ajout de seeds, blocage de domaine — protégés par `X-Admin-Token` |
135 +| `/embed` · `/rerank` | POST | Service d'embeddings + reranker interne (nœud m4mc, `services/embedding`) — jamais exposé publiquement |
101 136
102 −**Latences observées en prod** : recherche lexicale **20–95 ms** (21 ms sur « plombier gatineau », 29 ms sur l'onglet Images), **~70–270 ms** quand la recherche sémantique s'active (269 ms sur « cabane à sucre »).
137 +**Latences observées en prod (2026-08-24)** : **258–425 ms** avec recherche sémantique + reranking actifs (345 ms « poutine » · 4 395 résultats, 258 ms « resto québec » · 2 959 résultats, 425 ms « cabane à sucre » · 1 131 résultats). Le chemin lexical pur mesurait **20–95 ms** (21 ms « plombier gatineau », 29 ms onglet Images — captures de la phase web ouvert).
103 138
104 139 ## Architecture
105 140
106 −En production sur **M2M32**, l'application tourne en processus natifs sous **PM2**, avec l'infrastructure de données en **Docker** :
141 +Le moteur tourne en **architecture native distribuée sur 7 nœuds du cluster MacLustr**, coordonnée sans orchestrateur. Hub **M2M32** (Mac Studio, 12 cœurs, 32 Go) : processus natifs sous **PM2**, données en **Docker** ; l'index et la sémantique vivent sur des nœuds dédiés, reliés par tunnels SSH.
107 142
108 −| Processus | Rôle |
143 +### Processus PM2 sur M2M32 (commandes réelles)
144 +
145 +| Processus | Commande | Rôle |
146 +|---|---|---|
147 +| **tk-web** | `pnpm start` (cwd `apps/web`) | Web public **Next.js 15** (port **3000**) — proxifie `/api/*` vers l'API interne ; un seul port exposé |
148 +| **tk-api** | `.venv/bin/python -m uvicorn trouveka.api.main:app --host 0.0.0.0 --port 8080` | API **FastAPI** — recherche, statut, stats, soumission, admin (`X-Admin-Token`) |
149 +| **tk-crawler-1** / **tk-crawler-2** | `.venv/bin/python -m trouveka.crawler.worker` | Workers de crawl du hub : fetch → parse → score → indexation inline |
150 +| **tk-enrichment** | `.venv/bin/python -m trouveka.enrichment.worker` | Enrichissement asynchrone (autorité, entités, embeddings) via **Redis Streams** |
151 +| **tk-scheduler** | `.venv/bin/python -m trouveka.scheduler.loop` | Recrawls adaptatifs, entretien du frontier, rétention des données |
152 +| **tk-sitemaps** | `scripts/ka-sitemaps/ingest.py` | Ingestion quotidienne des sitemaps des 14 sites KA (streaming, priorités) — s'arrête entre deux passes |
153 +| **tk-tun-search** | `ssh -N -L 127.0.0.1:9210:localhost:9200 …@192.168.2.77` | Tunnel vers OpenSearch (M2M32b) |
154 +| **tk-tun-embed** | `ssh -N -L 127.0.0.1:8191:localhost:8091 …@192.168.2.75` | Tunnel vers embeddings + reranker (m4mc) |
155 +
156 +### Répartition sur le cluster
157 +
158 +| Nœud | Rôle |
109 159 |---|---|
110 −| **tk-web** | Application web publique **Next.js 15** (port **3000**) — proxifie `/api/*` vers l'API interne ; un seul port exposé |
111 −| **tk-api** | API **FastAPI** (`trouveka.api`) — recherche, statut, soumission d'URL, endpoints admin protégés (`X-Admin-Token`) |
112 −| **tk-crawler-1** / **tk-crawler-2** | Workers de crawl : fetch → parse → score Québec → indexation inline, politesse par hôte, garde SSRF |
113 −| **tk-enrichment** | Enrichissement asynchrone (autorité de domaine, entités, embeddings) via **Redis Streams** — ne bloque jamais l'indexation |
114 −| **tk-scheduler** | Planification des recrawls adaptatifs et entretien du frontier |
115 −| **postgres** (Docker, `postgres:16-alpine`) | Frontier, domaines, documents, graphe de liens, analytics |
116 −| **redis** (Docker, `redis:7-alpine`) | Verrous de politesse par hôte, pause du crawl, Redis Streams pour l'enrichissement |
117 −| **com.trouveka.ngrok** (launchd) | Tunnel `ngrok http --url=www.trouve-ka.com 3000` — exposition publique du port 3000 |
160 +| **M2M32** (hub) | PM2 ci-dessus + **postgres:16-alpine** et **redis:7-alpine** en Docker (frontier, domaines, documents, graphe de liens, analytics ; verrous de politesse, pause, Redis Streams) + tunnel **ngrok** (launchd `com.trouveka.ngrok`, `ngrok http --url=www.trouve-ka.com 3000`) |
161 +| **M2M32b** | **OpenSearch 2.17** en Docker (`trouveka-opensearch`, heap 8 G) — l'index, BM25 FR/EN + `function_score` Québec-first |
162 +| **m4mc** | Service **embeddings + reranker** (`services/embedding`, venv dédié avec torch, port 8091) |
163 +| **M2M32c** (×3), **M4BP48** (×2), **M1M32**, **m4ma** | Workers de crawl satellites (`trouveka.crawler.worker`) — se connectent à Postgres/Redis (192.168.2.90) et OpenSearch (192.168.2.77) par tunnels SSH bouclés (`trouveka-tunloop-*`) |
118 164
119 −La recherche s'appuie sur **OpenSearch 2.17** (BM25 FR/EN, synonymes bilingues, `function_score` Québec-first). Dégradation gracieuse partout : embeddings en panne → le lexical continue ; un worker crash → le frontier continue.
165 +Postgres est réglé pour cette flotte (`max_connections=200`, keepalives TCP, `idle_in_transaction_session_timeout` — un satellite tué net ne tient plus de verrous zombies). **Auto-guérison** : `io.trouveka.boot` (launchd, tous les nœuds), `io.trouveka.watchdog` (M2M32, toutes les 2 min), passe de flotte ~30 min, **backups quotidiens croisés** Postgres + OpenSearch (03:30–04:40). Dégradation gracieuse partout : embeddings en panne → le lexical continue ; un worker crash → le frontier continue.
166 +
167 +## Démarrage rapide (dev local)
168 +
169 +```bash
170 +# 1. Infrastructure de données (Postgres 16 + Redis 7 + OpenSearch 2.17.1)
171 +docker compose up -d postgres redis opensearch
172 +
173 +# 2. Backend Python (>= 3.12, package namespace trouveka.*)
174 +python3 -m venv .venv
175 +.venv/bin/pip install -e ".[dev]"
176 +
177 +# 3. Migrations + seeds (idempotent — les URL connues sont ignorées)
178 +pnpm crawl:seed # = scripts/bootstrap-seeds/seed.sh → trouveka.crawler.seed
179 +
180 +# 4. Frontend Next.js 15 (workspace pnpm)
181 +pnpm install
182 +pnpm dev # http://localhost:3000
183 +
184 +# 5. Tests + garde-fous
185 +.venv/bin/python -m pytest tests/ # 10 suites : fetcher, frontier, hybrid_search,
186 + # parser(+encoding), quebec_scoring, ranking,
187 + # ssrf, traps, urls
188 +pnpm check:headers # header auteur obligatoire (CI)
189 +```
190 +
191 +Scripts pnpm du monorepo : `dev` · `build` · `start` (app web), `crawl:seed`, `check:headers`, `deploy:m2m32`. Côté ops : `scripts/ka-sitemaps/ingest.py` (alimentation par sitemaps), `scripts/health-check/`, `scripts/eval/` (qualité de recherche), `scripts/update-synonyms/`, `scripts/migrate-search-index/`, `scripts/cleanup-garbage.py`, `scripts/fix-mojibake/`.
192 +
193 +## Variables d'environnement
194 +
195 +Noms seulement — voir `.env.example` (jamais de secrets dans le repo) :
196 +
197 +| Groupe | Variables |
198 +|---|---|
199 +| Bases de données | `DATABASE_URL`, `REDIS_URL`, `SEARCH_URL`, `SEARCH_URL_BACKEND`, `PG_PORT`, `REDIS_PORT` |
200 +| Crawler | `CRAWLER_USER_AGENT`, `CRAWLER_CONTACT_URL`, `MAX_GLOBAL_CONCURRENCY`, `MAX_PER_HOST_CONCURRENCY`, `DEFAULT_HOST_DELAY`, `MAX_RESPONSE_BYTES`, `MAX_REDIRECTS`, `FETCH_TIMEOUT`, `MAX_CRAWL_DEPTH`, `MAX_LINKS_PER_PAGE`, `MAX_URLS_PER_DOMAIN` |
201 +| API | `API_HOST`, `API_PORT`, `ADMIN_TOKEN` |
202 +| Web | `PUBLIC_URL`, `NGROK_DOMAIN`, `API_URL` |
203 +| Index & sémantique | `SEARCH_INDEX`, `EMBEDDING_URL` |
204 +| SSO Groupe KA | `KA_SSO_SECRET`, `KA_HUB_URL` |
205 +
206 +## Données & conformité
207 +
208 +- **Un robot qui se présente** : UA `TrouveKABot` avec URL de contact (`CRAWLER_CONTACT_URL`), page publique [/trouveka-bot](https://www.trouve-ka.com/trouveka-bot) qui explique qui il est, comment le bloquer ou l'inviter.
209 +- **robots.txt d'abord** : parsé avec Protego, mis en cache 24 h, `Crawl-delay` honoré ; politesse par hôte via verrou Redis (2 s par défaut), plafonds de concurrence globaux et par hôte.
210 +- **Provenance des données** : depuis le pivot du 2026-08-23, le moteur n'indexe que les **14 sites du Groupe KA** (`ka_only=True` dans `packages/config`) — du contenu de l'écosystème, découvert par seeds + sitemaps. La phase « web québécois ouvert » reste réactivable par configuration.
211 +- **Fraîcheur sans gaspillage** : ETag/If-Modified-Since, hash de contenu, recrawl adaptatif (×2 si inchangé, ÷2 si volatil) — on ne re-télécharge pas ce qui n'a pas bougé.
212 +- **Sécurité du fetch** : garde SSRF (IP privées, loopback, métadonnées cloud bloquées, revalidée à chaque redirection), taille de réponse et redirections plafonnées, détection de pièges de crawl.
213 +- **Images** : jamais crawlées ni copiées — hotlink avec attribution du domaine source.
214 +- **Rétention** : `crawl_attempts` 30 jours, `search_queries` 180 jours (purge quotidienne par le scheduler) ; backups quotidiens croisés Postgres + OpenSearch.
215 +- **Métriques honnêtes** : aucun compteur simulé nulle part — `/status`, `/stats`, les pastilles de ce README et les rapports PDF lisent tous les mêmes données réelles.
216 +- **Retrait** : blocage de domaine via `/api/admin/domains/block` — et `TrouveKABot` respecte tout `Disallow` qui le vise.
120 217
121 218 ## Documentation
122 219
123 220 - **Guide utilisateur illustré** : [www.trouve-ka.com/doc](https://www.trouve-ka.com/doc/index.html) — la visite en 4 étapes (accueil, recherche, images, état du moteur + soumission de site).
124 221 - **Guide PDF téléchargeable** : [trouve-ka-documentation.pdf](https://www.trouve-ka.com/doc/trouve-ka-documentation.pdf).
125 −- **Architecture** : [docs/architecture.md](docs/architecture.md) (diagrammes Mermaid) · **Décisions** : [docs/decisions.md](docs/decisions.md) · **Runbook** : [docs/RUNBOOK.md](docs/RUNBOOK.md).
222 +- **Architecture** : [docs/architecture.md](docs/architecture.md) (diagrammes Mermaid) · **Décisions** : [docs/decisions.md](docs/decisions.md) · **Runbook** : [docs/RUNBOOK.md](docs/RUNBOOK.md) (auto-guérison, backups, journaux, procédures).
126 223 - **Le robot** : [/trouveka-bot](https://www.trouve-ka.com/trouveka-bot) — qui est `TrouveKABot`, comment le bloquer ou l'inviter.
127 224
128 225 ## Structure du repo
@@ -133,13 +230,15 @@ services/ # crawler, frontier, parser, classifier, indexer, ranking,
133 230 # scheduler, enrichment, embedding
134 231 packages/ # config, database, logging, queue, search-core, shared, types
135 232 infrastructure/ # docker/, migrations/, monitoring/, deployment/
136 −scripts/ # bootstrap-seeds, start-crawler, health-check, eval, ops,
137 − # ka-sitemaps, update-synonyms, check-headers.py…
138 −tests/ # unitaires : canonicalisation, SSRF, robots, scoring Québec, ranking…
233 +scripts/ # bootstrap-seeds, ka-sitemaps, start-crawler, health-check, eval,
234 + # ops, update-synonyms, migrate-search-index, cleanup-garbage,
235 + # fix-mojibake, check-headers.py
236 +tests/ # 10 suites : canonicalisation d'URL, SSRF, robots/frontier,
237 + # scoring Québec, ranking, recherche hybride, parseur, encodage, pièges
139 238 docs/ # architecture.md, decisions.md, RUNBOOK.md, screenshots/
140 239 ```
141 240
142 −Le backend Python est un seul package namespace `trouveka.*` mappé sur ce layout (voir `pyproject.toml`) ; le frontend vit dans un workspace **pnpm**.
241 +Le backend Python est un seul package namespace `trouveka.*` mappé sur ce layout (voir `pyproject.toml` — FastAPI, httpx, selectolax, protego, asyncpg, redis, opensearch-py, pydantic v2) ; le frontend vit dans un workspace **pnpm**.
143 242
144 243 ## Développement (remote-first)
145 244
@@ -162,7 +261,7 @@ pnpm build # build de apps/web
162 261
163 262 # Appliquer un changement
164 263 pm2 restart tk-web # ou tk-api, tk-crawler-1, tk-crawler-2,
165 − # tk-enrichment, tk-scheduler
264 + # tk-enrichment, tk-scheduler, tk-sitemaps
166 265
167 266 git add <fichiers> && git commit -m "…" && git push origin main
168 267 ```
@@ -171,39 +270,58 @@ Dev local complet possible : `docker compose up -d postgres redis opensearch`, `
171 270
172 271 ## Déploiement
173 272
174 −- **Nœud** : **M2M32** (`~/trouve-ka`) — Mac Studio, 12 cœurs, 32 Go.
273 +- **Nœud** : **M2M32** (`~/trouve-ka`) — Mac Studio, 12 cœurs, 32 Go — plus M2M32b (index), m4mc (sémantique) et 4 satellites de crawl (voir Architecture).
175 274 - **Web** : `tk-web` sur le port **3000**, exposé sur **https://www.trouve-ka.com**.
176 −- **Processus** : **PM2** (`pm2 ls` → tk-web, tk-api, tk-crawler-1/2, tk-enrichment, tk-scheduler) — auto-restart, survie au reboot.
177 −- **Données** : **PostgreSQL 16** + **Redis 7** en **Docker** (`docker compose up -d postgres redis`).
275 +- **Processus** : **PM2** (`pm2 ls` → tk-web, tk-api, tk-crawler-1/2, tk-enrichment, tk-scheduler, tk-sitemaps, tk-tun-search, tk-tun-embed) — auto-restart, survie au reboot.
276 +- **Données** : **PostgreSQL 16** + **Redis 7** en **Docker** sur M2M32 (`docker compose up -d postgres redis`) ; **OpenSearch 2.17** en Docker sur **M2M32b**.
178 277 - **Tunnel** : **ngrok** sous **launchd** — label `com.trouveka.ngrok` (`~/Library/LaunchAgents/com.trouveka.ngrok.plist`).
278 +- **Auto-guérison** : launchd `io.trouveka.boot` (tous les nœuds), `io.trouveka.watchdog` (2 min), backups `io.trouveka.backup-pg`/`backup-os` (quotidiens) — journaux `~/trouveka-watchdog.log`, `~/trouveka-boot.log`, `~/trouveka-backups/backup.log`.
179 279
180 280 ```bash
181 281 ssh M2M32
182 282 cd ~/trouve-ka
183 283 pm2 ls # état des services
184 284 pm2 logs tk-crawler-1 # crawl en direct
185 −docker ps # postgres + redis
285 +docker ps # postgres + redis (opensearch : docker ps sur M2M32b)
186 286 ```
187 287
188 288 Runbook et observabilité : [docs/RUNBOOK.md](docs/RUNBOOK.md), dashboard `/admin` (files, débits, latences p50/p95, flux live) et [infrastructure/monitoring/](infrastructure/monitoring/).
189 289
190 −## Écosystème Groupe KA
290 +## Historique
291 +
292 +28 commits en 11 jours — de la première ligne au moteur distribué du Groupe KA :
191 293
192 −| Plateforme | Vocation |
294 +| Date | Jalon |
193 295 |---|---|
194 −| [groupe-ka.com](https://www.groupe-ka.com) | Portail du Groupe KA |
195 −| [lou-ka.com](https://www.lou-ka.com) | Logements à louer |
196 −| [immo-ka.com](https://www.immo-ka.com) | Propriétés à vendre |
197 −| [vrai-prix.com](https://www.vrai-prix.com) | Estimation immobilière |
198 −| [auto-ka.com](https://www.auto-ka.com) | Véhicules |
199 −| [fabri-ka.com](https://www.fabri-ka.com) | Produits québécois |
200 −| [food-ka.com](https://www.food-ka.com) | Épicerie et alimentation |
201 −| [resto-ka.com](https://www.resto-ka.com) | Restaurants |
202 −| [sorti-ka.com](https://www.sorti-ka.com) | Sorties et événements |
203 −| [job-ka.com](https://www.job-ka.com) | Emplois |
204 −| [crea-ka.com](https://www.crea-ka.com) | Créateurs |
205 −| [trouve-ka.com](https://www.trouve-ka.com) | Petites annonces et recherche — **ce repo** |
206 −| [api-ka.com](https://www.api-ka.com) | API de données |
296 +| 2026-08-13 | **Naissance** (`5eedcb2`) — moteur de recherche Québec-first complet : crawler, index, ranking, API, web |
297 +| 2026-08-16 | **Recherche hybride sémantique + architecture native multi-nœuds** (`708a9ba`) · auto-guérison, survie au reboot, backups croisés, rétention (`78ddae6`) |
298 +| 2026-08-17 | **Design Groupe KA + SSO KA ID** (`bf69142`) · tableau de bord `/stats` + rapport PDF (`93cbd33`) · widget KA Agent (`1676a98`) |
299 +| 2026-08-19 | **Mobile-first** (hamburger, safe-area, dvh — `37a4bf7`) · KA Agent v2 plein écran + Markdown streaming (`8bdbe5f`) |
300 +| 2026-08-22 | **SEO complet** — canonical, JSON-LD WebSite/SearchAction, sitemap, fr-CA (`491f0f6`) · standard header/menu KA (`8678110`) |
301 +| 2026-08-23 | **Pivot Groupe KA** (`42bf528`) — le moteur n'indexe plus que les 14 sites KA ; rebrand « le moteur de recherche du Groupe KA » (`fd0b68c`) · stats v3, rapports PDF personnalisés (`105720d`) · favoris « Mon univers Ka » (`1e29ebf`) |
302 +| 2026-08-24 | **Documentation publique** `/doc` + guide PDF (`8880849`) · README v2 puis v3 — l'index passe de 181 k à 302 k pages dans la journée |
303 +
304 +## Écosystème Groupe KA
305 +
306 +Les 14 sites marqués ● sont ceux que le moteur indexe en continu (les seeds du frontier) :
307 +
308 +| Plateforme | Vocation | Indexé |
309 +|---|---|---|
310 +| [groupe-ka.com](https://www.groupe-ka.com) | Portail du Groupe KA | ● |
311 +| [trouve-ka.com](https://www.trouve-ka.com) | **Moteur de recherche — ce repo** | ● |
312 +| [lou-ka.com](https://www.lou-ka.com) | Logements à louer | ● |
313 +| [immo-ka.com](https://www.immo-ka.com) | Propriétés à vendre | ● |
314 +| [vrai-prix.com](https://www.vrai-prix.com) | Estimation immobilière | ● |
315 +| [toit-ka.com](https://www.toit-ka.com) | Louer ou acheter — les annonces réunies | ● |
316 +| [valoplex.com](https://www.valoplex.com) | Valeur immobilière, sans boîte noire | ● |
317 +| [auto-ka.com](https://www.auto-ka.com) | Véhicules | ● |
318 +| [fabri-ka.com](https://www.fabri-ka.com) | Produits québécois | ● |
319 +| [food-ka.com](https://www.food-ka.com) | Épicerie et alimentation | ● |
320 +| [resto-ka.com](https://www.resto-ka.com) | Restaurants | ● |
321 +| [sorti-ka.com](https://www.sorti-ka.com) | Sorties et événements | ● |
322 +| [job-ka.com](https://www.job-ka.com) | Emplois | ● |
323 +| [crea-ka.com](https://www.crea-ka.com) | Créateurs | ● |
324 +| [api-ka.com](https://www.api-ka.com) | API de données (technique, non indexée) | — |
207 325
208 326 ## Contact
209 327
210 328