API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com
Python 60.9%
HTML 21%
TypeScript 7.3%
JavaScript 5.2%
CSS 4.8%
Shell 0.8%
1# ============================================2# Projet : API-KA3# Fichier : src/api/routes/search.py4# Node : m3u96b5# Author : Simon-Pierre Boucher6# Contact : contact@spboucher.ai7# Date : 2026-08-238# ============================================9"""Routes /api/v1/search et /api/v1/suggest — recherche Trouve·Ka.1011Proxy public du moteur de recherche interne du Groupe KA (www.trouve-ka.com) :12API-KA relaie les requêtes vers le moteur amont et renvoie ses résultats dans13l'enveloppe uniforme ``{success, data, meta}`` de la plateforme.14"""1516from __future__ import annotations1718from typing import Any1920import httpx21from fastapi import APIRouter, HTTPException, Query2223from src.api.routes import envelope24from src.config import get_settings2526router = APIRouter(prefix="/api/v1", tags=["search"])2728# Le moteur Trouve·Ka plafonne `limit` à 50 — même plafond ici.29MAX_LIMIT = 50303132async def _get(url: str, params: dict[str, Any]) -> Any:33 """GET JSON vers le moteur Trouve·Ka (mêmes réglages que le proxy agent)."""34 async with httpx.AsyncClient(timeout=12, follow_redirects=True) as cx:35 r = await cx.get(url, params=params)36 r.raise_for_status()37 return r.json()383940@router.get("/search")41async def search(42 q: str = Query(43 ..., min_length=1, max_length=200, description="Termes de recherche"44 ),45 page: int = Query(1, ge=1, description="Numéro de page (défaut 1)"),46 limit: int = Query(47 10, ge=1, le=MAX_LIMIT, description="Résultats par page (défaut 10, max 50)"48 ),49 language: str | None = Query(50 None, pattern="^(fr|en)$", description="Langue des résultats : fr ou en"51 ),52 site: str | None = Query(53 None,54 max_length=253,55 description="Limiter à un site — domaine, ex. www.lou-ka.com",56 ),57 category: str | None = Query(None, description="Catégorie Trouve·Ka"),58 freshness: str | None = Query(59 None,60 pattern="^(day|week|month|year)$",61 description="Fraîcheur : day, week, month ou year",62 ),63) -> dict[str, Any]:64 """Recherche web québécoise propulsée par le moteur Trouve·Ka.6566 Proxifie ``GET https://www.trouve-ka.com/api/search`` (moteur de recherche67 interne du Groupe KA) : résultats classés (sémantique + rerank), filtrables68 par langue, site, catégorie et fraîcheur. ``data`` contient les résultats69 (``title``, ``url``, ``snippet``, ``domain``, ``quebec_score``…) ; ``meta``70 expose ``total``, ``took_ms`` et les requêtes reliées (``related``).71 """72 params: dict[str, Any] = {"q": q, "page": page, "limit": limit}73 for key, value in (74 ("language", language),75 ("site", site),76 ("category", category),77 ("freshness", freshness),78 ):79 if value:80 params[key] = value81 try:82 data = await _get(get_settings().trouveka_search_url, params)83 except httpx.HTTPError as exc:84 raise HTTPException(85 status_code=502, detail=f"Moteur Trouve·Ka injoignable : {exc}"86 ) from exc87 return envelope(88 data.get("results", []),89 total=data.get("total"),90 page=data.get("page", page),91 limit=data.get("limit", limit),92 extra_meta={93 "query": data.get("query", q),94 "took_ms": data.get("took_ms"),95 "related": data.get("related", []),96 "source": "trouve-ka",97 },98 )99100101@router.get("/suggest")102async def suggest(103 q: str = Query(104 ...,105 min_length=2,106 max_length=200,107 description="Préfixe de recherche (2 caractères minimum)",108 ),109) -> dict[str, Any]:110 """Suggestions de recherche (autocomplétion) du moteur Trouve·Ka.111112 Proxifie ``GET https://www.trouve-ka.com/api/suggest`` : renvoie dans113 ``data`` la liste des suggestions de requêtes pour le préfixe donné.114 """115 try:116 data = await _get(get_settings().trouveka_suggest_url, {"q": q})117 except httpx.HTTPError as exc:118 raise HTTPException(119 status_code=502, detail=f"Moteur Trouve·Ka injoignable : {exc}"120 ) from exc121 return envelope(data.get("suggestions", []), extra_meta={"source": "trouve-ka"})122