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%

feat(ka-id v2.1): reclassement personnalisé léger de la fenêtre de résultats (localisation transversale du profil KA ID, blend 0.25, fail-open) + champ personalized

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

1 changed file +74 −1

modified apps/api/main.py +74 −1
@@ -204,6 +204,73 @@ def _kaid_push(ka_id: str, event: dict) -> None:
204 204 pass # best-effort
205 205
206 206
207 +_KA_APPS = {"lou-ka", "immo-ka", "vrai-prix", "valoplex", "auto-ka",
208 + "fabri-ka", "food-ka", "resto-ka", "sorti-ka", "crea-ka",
209 + "job-ka", "groupe-ka", "api-ka", "toit-ka", "ora-ka"}
210 +_kaid_prefs_cache: dict = {}
211 +
212 +
213 +def _kaid_prefs(ka_id: str) -> dict | None:
214 + """Profil de personnalisation du hub (cache 90 s, best-effort)."""
215 + now = _time.time()
216 + hit = _kaid_prefs_cache.get(ka_id)
217 + if hit and now - hit[0] < 90:
218 + return hit[1]
219 + secret = _os.environ.get("KA_SSO_SECRET")
220 + data = None
221 + if secret:
222 + ts = str(int(now))
223 + sig = _hmac.new(secret.encode(), f"{_KAID_CLIENT}.{ka_id}.{ts}".encode(),
224 + _hashlib.sha256).hexdigest()
225 + url = (f"{_KA_HUB}/api/sso/prefs?client_id={_KAID_CLIENT}"
226 + f"&ka_id={ka_id}&ts={ts}&sig={sig}")
227 + try:
228 + with _urlreq.urlopen(url, timeout=4) as r:
229 + data = _json.loads(r.read())
230 + except Exception:
231 + data = None
232 + _kaid_prefs_cache[ka_id] = (now, data)
233 + if len(_kaid_prefs_cache) > 300:
234 + for k in list(_kaid_prefs_cache)[:100]:
235 + del _kaid_prefs_cache[k]
236 + return data
237 +
238 +
239 +def _kaid_boost(request: Request, results: list) -> bool:
240 + """Reclassement personnalisé LÉGER de la fenêtre de page : les pages des
241 + univers KA que le membre utilise le plus, et celles qui mentionnent ses
242 + coins du Québec, remontent doucement (blend 0.25 — la pertinence du
243 + moteur reste dominante). Fail-open : à la moindre absence, ordre intact."""
244 + if len(results) < 2:
245 + return False
246 + ka_id = _tk_ka_id(request)
247 + if not ka_id:
248 + return False
249 + prefs = _kaid_prefs(ka_id)
250 + if not prefs or not prefs.get("personalization"):
251 + return False
252 + profile = prefs.get("profile") or {}
253 + # le hub ne nous transmet que notre bloc d'univers + le transversal :
254 + # on exploite la localisation transversale (villes/quartiers du membre).
255 + gl = (profile.get("global") or {}).get("location") or {}
256 + loc_values = {k: v for k, v in (gl.get("values") or {}).items() if v > 0}
257 + loc_conf = float(gl.get("conf") or 0.3)
258 + if not loc_values:
259 + return False
260 + n = len(results)
261 + scored = []
262 + for i, r in enumerate(results):
263 + base = 1.0 - i / n
264 + text = ((r.get("title") or "") + " " + (r.get("snippet") or "")).lower()
265 + loc = max((aff for val, aff in loc_values.items() if val in text),
266 + default=0.0)
267 + p = 0.5 + 0.5 * loc * loc_conf
268 + scored.append((0.75 * base + 0.25 * p, i, r))
269 + scored.sort(key=lambda t: (-t[0], t[1]))
270 + results[:] = [r for _, _, r in scored]
271 + return True
272 +
273 +
207 274 def _kaid_track_search(request: Request, q: str, filters: dict) -> None:
208 275 ka_id = _tk_ka_id(request)
209 276 if not ka_id:
@@ -347,8 +414,14 @@ async def api_search(
347 414 except Exception:
348 415 related = []
349 416
417 + # KA ID v2 : reclassement personnalisé léger de la fenêtre courante
418 + try:
419 + personalized = _kaid_boost(request, results)
420 + except Exception:
421 + personalized = False
350 422 return {"query": q, "total": total, "took_ms": took_ms, "page": page, "limit": limit,
351 − "semantic": semantic, "reranked": reranked, "related": related, "results": results}
423 + "semantic": semantic, "reranked": reranked, "related": related,
424 + "personalized": personalized, "results": results}
352 425
353 426
354 427 @app.get("/api/status")
355 428