// SRC-04: Backend-for-Frontend-Route für die Such-Oberfläche. Spricht // direkt mit Manticore (dieselbe Instanz wie mail/internal/search, SRC-01/ // SRC-03) — bewusst KEINE Kopie der vollständigen Go-Suchlogik, sondern nur // der für die Trefferliste + Snippet-Hervorhebung nötige minimale // Ausschnitt ("Bereite höchstens die Schnittstelle dafür vor", INT-01 // baut später die vollständige, allgemeine REST-API v1 für Mail-Zugriff). // // Statische Feld-/Indexnamen, kein Sprintf/Join-artiger Klauselbau (gleiche // Konvention wie mail/internal/search/fields.go). import { NextRequest, NextResponse } from "next/server"; import { HIGHLIGHT_AFTER, HIGHLIGHT_BEFORE } from "../../../lib/highlight"; const INDEX_NAME = "mail_documents"; const FIELD_TENANT_SLUG = "tenant_slug"; function manticoreURL(): string { const base = process.env.MANTICORE_URL; if (!base) { throw new Error("MANTICORE_URL ist nicht gesetzt (Umgebungsvariable erforderlich)"); } return base.replace(/\/$/, ""); } interface ManticoreHit { _score: number; _source: { message_id: string }; highlight?: { subject?: string[]; body?: string[] }; } interface ManticoreSearchResponse { hits?: { hits?: ManticoreHit[] }; error?: string; } export async function GET(request: NextRequest) { const tenantSlug = request.nextUrl.searchParams.get("tenant"); const query = request.nextUrl.searchParams.get("q"); if (!tenantSlug || !query) { return NextResponse.json({ error: "'tenant' und 'q' sind erforderlich" }, { status: 400 }); } const manticorePayload = { index: INDEX_NAME, query: { bool: { must: [{ equals: { [FIELD_TENANT_SLUG]: tenantSlug } }, { query_string: query }], }, }, highlight: { fields: { subject: {}, body: {} }, before_match: HIGHLIGHT_BEFORE, after_match: HIGHLIGHT_AFTER, }, }; let manticoreResponse: Response; try { manticoreResponse = await fetch(`${manticoreURL()}/search`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(manticorePayload), }); } catch (err) { return NextResponse.json({ error: `Suche nicht erreichbar: ${(err as Error).message}` }, { status: 502 }); } const parsed: ManticoreSearchResponse = await manticoreResponse.json().catch(() => ({})); if (!manticoreResponse.ok || parsed.error) { return NextResponse.json({ error: parsed.error ?? "Suche fehlgeschlagen" }, { status: 502 }); } const hits = (parsed.hits?.hits ?? []).map((hit) => ({ messageId: hit._source.message_id, subjectSnippet: hit.highlight?.subject?.[0] ?? "", bodySnippet: hit.highlight?.body?.[0] ?? "", score: hit._score, })); return NextResponse.json({ hits }); }