/** * /api/search/hybrid — Public hybrid search endpoint (no auth required). * * Used by the global Cmd+K command palette + any external integration. * Returns the same shape as the chat tool but exposed via HTTP. * * GET /api/search/hybrid?q=...&lang=pt&doc_id=...&top_k=10 */ import { NextRequest } from "next/server"; import { hybridSearch } from "@/lib/retrieval/hybrid"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; function json(data: unknown, status = 200) { return new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" }, }); } export async function GET(req: NextRequest) { const u = new URL(req.url); const q = u.searchParams.get("q")?.trim(); if (!q) return json({ error: "q required", hits: [] }, 400); const lang = (u.searchParams.get("lang") === "en" ? "en" : "pt") as "pt" | "en"; const doc_id = u.searchParams.get("doc_id") || null; const type = u.searchParams.get("type") || null; const top_k = Math.min(Number(u.searchParams.get("top_k") ?? 10), 50); const ufo_only = u.searchParams.get("ufo_only") === "1"; // W2-TD#8: rerank now has three modes. Back-compat: `rerank=0` keeps the // old "never" shortcut. New: `rerank=always|when_top_k_gt|never` and // `rerank_threshold=N` (default 15). Default strategy is `when_top_k_gt`. const rerankParam = u.searchParams.get("rerank"); const no_rerank = rerankParam === "0"; const rerank_strategy = ( rerankParam === "always" || rerankParam === "never" || rerankParam === "when_top_k_gt" ? rerankParam : "when_top_k_gt" ) as "always" | "when_top_k_gt" | "never"; const rerank_threshold = Math.max( 1, Math.min(50, Number(u.searchParams.get("rerank_threshold") ?? 15)), ); try { const hits = await hybridSearch({ query: q, lang, doc_id, type, ufo_only, top_k, no_rerank, rerank_strategy, rerank_threshold, }); return json({ query: q, lang, count: hits.length, hits: hits.map((h) => ({ chunk_id: h.chunk_id, doc_id: h.doc_id, page: h.page, type: h.type, bbox: h.bbox, classification: h.classification, snippet: ((lang === "en" ? h.content_en : h.content_pt) || "").slice(0, 280), score: Number((h.rerank_score ?? h.score).toFixed(4)), href: `/d/${h.doc_id}#${h.chunk_id}`, })), }); } catch (e) { return json({ error: "retrieval_unavailable", message: (e as Error).message }, 503); } }