"""SearXNG bridge — runs a SearXNG metasearch instance inside the Space container (userspace venv, no root) and proxies queries to it. Mounted under /respite/v2/searx/*: /status → bootstrap state of the runtime, and of the reranker /search → proxy ?q=... queries to SearXNG's JSON API /rerank → score/reorder/drop a result set for a query (see rerank.py) Engine selection comes from searxng_settings.yml (DDG + Google enabled); the frontend picks engines per request via the standard `engines` param. /rerank lives under the same prefix as /search on purpose: the Space's key gate guards /respite/v2/searx/*, so the reranker inherits that authentication rather than needing a second one to be got right. """ import asyncio import json import httpx from fastapi import Request from fastapi.responses import JSONResponse import rerank import searx_runtime # Load the reranker's checkpoint as soon as the container boots, not on the # first search request. The first request has a ~1s deadline and the load takes # minutes, so without this the reranker can never apply on a cold container and # the only symptom is a timeout. A daemon thread, so a failure or a slow mirror # cannot hold the Space's startup hostage. rerank.warm() def register_routes(fa_app): @fa_app.get("/respite/v2/searx/status") def _status(): return JSONResponse({ "runtime_dir": searx_runtime.RUNTIME_DIR, "installed": searx_runtime.searxng_ready(), "boot_error": searx_runtime.boot_error(), "port": searx_runtime.SEARXNG_PORT, "reranker": rerank.status(), }) @fa_app.get("/respite/v2/searx/search") async def _search(request: Request): if not searx_runtime.searxng_ready(): ok = searx_runtime.start_searxng() if not ok: return JSONResponse({"error": "searxng not running"}, status_code=503) params = dict(request.query_params) # SearXNG's JSON API lives at /search with format=json params.setdefault("format", "json") url = f"http://127.0.0.1:{searx_runtime.SEARXNG_PORT}/search" try: async with httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=5.0)) as client: r = await client.get(url, params=params) return JSONResponse(r.json(), status_code=r.status_code) except Exception as e: return JSONResponse({"error": str(e)}, status_code=502) @fa_app.post("/respite/v2/searx/rerank") async def _rerank(request: Request): """Rerank a result set. Always answers 200 with the results in some order — the client is a search path, and an exception here would look like an outage to whoever is trying to read the page.""" try: body = await request.body() payload = json.loads(body or b"{}") except Exception as e: return JSONResponse({"ok": False, "error": f"bad request: {e}", "results": []}) q = str(payload.get("q") or "").strip() results = payload.get("results") if not q or not isinstance(results, list) or not results: return JSONResponse({"ok": False, "error": "q and results are required", "results": results or []}) budget_ms = int(payload.get("budget_ms") or 800) # rerank() is synchronous and CPU-bound (torch), so it must not run on # the event loop — that would stall every other Space request, audio # generation included, for as long as the model takes. try: out = await asyncio.to_thread(rerank.rerank, q, results, budget_ms) except Exception as e: # noqa: BLE001 # `no_opinion` as well as `error`: the client reports the former, so # a crash that never reached rerank() must not read as "the model # had nothing to say about your query". return JSONResponse( { "ok": False, "error": f"rerank failed: {e}", "no_opinion": f"{type(e).__name__}: {e}"[:300], "results": results, } ) return JSONResponse(out) @fa_app.get("/respite/v2/searx/rerank/recent") def _rerank_recent(limit: int = 20): """Recent relevance scores, for choosing DROP_BELOW from real traffic rather than from a guess.""" return JSONResponse({"scores": rerank.recent_scores(limit)})