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
Python 51.9%
TypeScript 32.7%
JavaScript 4.8%
CSS 4.1%
Shell 2.9%
HTML 1.8%
SQL 1.5%
1# Trouve-KA — client du service d'embeddings2# Author: Simon-Pierre Boucher3# Contact: contact@spboucher.ai45"""Client HTTP du service d'embeddings — conçu pour la dégradation gracieuse (§13).67Ne lève JAMAIS : toute erreur (service absent, timeout, réponse invalide)8retourne None et le chemin lexical continue seul. Timeout requête court pour9ne pas pénaliser la latence de recherche; timeout passage plus généreux pour10l'enrichissement en arrière-plan.11"""1213import httpx141516class EmbedClient:17 def __init__(18 self,19 base_url: str | None,20 *,21 query_timeout: float = 0.25,22 passage_timeout: float = 30.0,23 rerank_timeout: float = 0.6,24 ):25 self.base_url = (base_url or "").rstrip("/") or None26 self._query_client = httpx.AsyncClient(timeout=query_timeout) if self.base_url else None27 self._passage_client = httpx.AsyncClient(timeout=passage_timeout) if self.base_url else None28 self._rerank_client = httpx.AsyncClient(timeout=rerank_timeout) if self.base_url else None2930 @property31 def enabled(self) -> bool:32 return self.base_url is not None3334 async def close(self) -> None:35 for c in (self._query_client, self._passage_client, self._rerank_client):36 if c is not None:37 await c.aclose()3839 async def rerank(self, query: str, texts: list[str]) -> list[float] | None:40 """Scores cross-encoder pour le top-N, ou None (l'ordre RRF est conservé)."""41 if not self.base_url or self._rerank_client is None or not texts:42 return None43 try:44 resp = await self._rerank_client.post(45 f"{self.base_url}/rerank", json={"query": query, "texts": texts}46 )47 resp.raise_for_status()48 scores = resp.json()["scores"]49 if not isinstance(scores, list) or len(scores) != len(texts):50 return None51 return [float(s) for s in scores]52 except Exception:53 return None5455 async def embed_query(self, text: str) -> list[float] | None:56 """Vecteur de la requête utilisateur, ou None (fallback lexical)."""57 return await self._embed([text], kind="query", client=self._query_client, single=True)5859 async def embed_passages(self, texts: list[str]) -> list[list[float]] | None:60 """Vecteurs de passages (enrichissement asynchrone), ou None."""61 if not texts:62 return []63 return await self._embed(texts, kind="passage", client=self._passage_client, single=False)6465 async def _embed(self, texts: list[str], *, kind: str, client: httpx.AsyncClient | None, single: bool):66 if not self.base_url or client is None:67 return None68 try:69 resp = await client.post(f"{self.base_url}/embed", json={"texts": texts, "kind": kind})70 resp.raise_for_status()71 vectors = resp.json()["vectors"]72 if not isinstance(vectors, list) or len(vectors) != len(texts):73 return None74 return vectors[0] if single else vectors75 except Exception:76 return None77