# ============================================ # Projet : API-KA # Fichier : src/api/routes/search.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-23 # ============================================ """Routes /api/v1/search et /api/v1/suggest — recherche Trouve·Ka. Proxy public du moteur de recherche interne du Groupe KA (www.trouve-ka.com) : API-KA relaie les requêtes vers le moteur amont et renvoie ses résultats dans l'enveloppe uniforme ``{success, data, meta}`` de la plateforme. """ from __future__ import annotations from typing import Any import httpx from fastapi import APIRouter, HTTPException, Query from src.api.routes import envelope from src.config import get_settings router = APIRouter(prefix="/api/v1", tags=["search"]) # Le moteur Trouve·Ka plafonne `limit` à 50 — même plafond ici. MAX_LIMIT = 50 async def _get(url: str, params: dict[str, Any]) -> Any: """GET JSON vers le moteur Trouve·Ka (mêmes réglages que le proxy agent).""" async with httpx.AsyncClient(timeout=12, follow_redirects=True) as cx: r = await cx.get(url, params=params) r.raise_for_status() return r.json() @router.get("/search") async def search( q: str = Query( ..., min_length=1, max_length=200, description="Termes de recherche" ), page: int = Query(1, ge=1, description="Numéro de page (défaut 1)"), limit: int = Query( 10, ge=1, le=MAX_LIMIT, description="Résultats par page (défaut 10, max 50)" ), language: str | None = Query( None, pattern="^(fr|en)$", description="Langue des résultats : fr ou en" ), site: str | None = Query( None, max_length=253, description="Limiter à un site — domaine, ex. www.lou-ka.com", ), category: str | None = Query(None, description="Catégorie Trouve·Ka"), freshness: str | None = Query( None, pattern="^(day|week|month|year)$", description="Fraîcheur : day, week, month ou year", ), ) -> dict[str, Any]: """Recherche web québécoise propulsée par le moteur Trouve·Ka. Proxifie ``GET https://www.trouve-ka.com/api/search`` (moteur de recherche interne du Groupe KA) : résultats classés (sémantique + rerank), filtrables par langue, site, catégorie et fraîcheur. ``data`` contient les résultats (``title``, ``url``, ``snippet``, ``domain``, ``quebec_score``…) ; ``meta`` expose ``total``, ``took_ms`` et les requêtes reliées (``related``). """ params: dict[str, Any] = {"q": q, "page": page, "limit": limit} for key, value in ( ("language", language), ("site", site), ("category", category), ("freshness", freshness), ): if value: params[key] = value try: data = await _get(get_settings().trouveka_search_url, params) except httpx.HTTPError as exc: raise HTTPException( status_code=502, detail=f"Moteur Trouve·Ka injoignable : {exc}" ) from exc return envelope( data.get("results", []), total=data.get("total"), page=data.get("page", page), limit=data.get("limit", limit), extra_meta={ "query": data.get("query", q), "took_ms": data.get("took_ms"), "related": data.get("related", []), "source": "trouve-ka", }, ) @router.get("/suggest") async def suggest( q: str = Query( ..., min_length=2, max_length=200, description="Préfixe de recherche (2 caractères minimum)", ), ) -> dict[str, Any]: """Suggestions de recherche (autocomplétion) du moteur Trouve·Ka. Proxifie ``GET https://www.trouve-ka.com/api/suggest`` : renvoie dans ``data`` la liste des suggestions de requêtes pour le préfixe donné. """ try: data = await _get(get_settings().trouveka_suggest_url, {"q": q}) except httpx.HTTPError as exc: raise HTTPException( status_code=502, detail=f"Moteur Trouve·Ka injoignable : {exc}" ) from exc return envelope(data.get("suggestions", []), extra_meta={"source": "trouve-ka"})