import { NextResponse } from "next/server"; import { json } from "@/lib/json"; import { getFileUrl } from "@/lib/minio"; import { prisma } from "@/lib/prisma"; import { buildSnippet, highlightAll, parseSearchQuery } from "@/lib/search"; import { searchRows } from "@/lib/search-query"; import { isVideoContentTag } from "@/lib/video-tags"; export async function GET(req: Request) { const { searchParams } = new URL(req.url); const q = (searchParams.get("q") || "").trim(); const rawTag = (searchParams.get("tag") || "").trim(); const contentTag = rawTag && isVideoContentTag(rawTag) ? rawTag : null; const page = Math.max(1, Math.floor(Number(searchParams.get("page")) || 1)); const limit = Math.min( 50, Math.max(1, Math.floor(Number(searchParams.get("limit")) || 20)), ); const offset = (page - 1) * limit; if (rawTag && !contentTag) { return NextResponse.json({ error: "Invalid tag" }, { status: 400 }); } const { terms, excludes } = parseSearchQuery(q); if (terms.length === 0 && !contentTag) { return json({ results: [], total: 0, page, limit, terms }); } try { const rows = await searchRows( { terms, excludes, contentTag }, offset, limit, ); const videoIds = rows .filter((r) => r.type === "video") .map((r) => r.awemeId); const imagePostIds = rows .filter((r) => r.type === "image") .map((r) => r.awemeId); const [videos, imagePosts] = await Promise.all([ videoIds.length > 0 ? prisma.video.findMany({ where: { aweme_id: { in: videoIds } }, select: { aweme_id: true, desc: true, cover_url: true, video_url: true, duration_ms: true, author: true, }, }) : [], imagePostIds.length > 0 ? prisma.imagePost.findMany({ where: { aweme_id: { in: imagePostIds } }, select: { aweme_id: true, desc: true, author: true, images: { orderBy: { order: "asc" }, take: 1, select: { url: true, width: true, height: true }, }, }, }) : [], ]); const videoMap = new Map( videos.map((v) => [ v.aweme_id, { ...v, cover_url: getFileUrl(v.cover_url || ""), video_url: getFileUrl(v.video_url || ""), author: { ...v.author, avatar_url: getFileUrl(v.author.avatar_url || ""), }, }, ]), ); const imagePostMap = new Map( imagePosts.map((ip) => [ ip.aweme_id, { ...ip, author: { ...ip.author, avatar_url: getFileUrl(ip.author.avatar_url || ""), }, cover_url: ip.images[0] ? getFileUrl(ip.images[0].url) : null, }, ]), ); return json({ results: rows.map((r) => { const matchedIn: ("desc" | "transcript")[] = []; if (r.inDesc) matchedIn.push("desc"); if (r.inTranscript) matchedIn.push("transcript"); return { id: `${r.type}:${r.awemeId}`, awemeId: r.awemeId, type: r.type, score: r.score, matchedIn, contentTags: r.contentTags, descParts: highlightAll(r.rawDesc, terms), // 转写命中时给出命中片段;只按标签筛选时给出转写开头作为预览 snippet: r.inTranscript || (terms.length === 0 && r.rawTranscript) ? buildSnippet(r.rawTranscript, terms) : null, video: r.type === "video" ? videoMap.get(r.awemeId) : undefined, imagePost: r.type === "image" ? imagePostMap.get(r.awemeId) : undefined, }; }), total: rows[0]?.total ?? 0, page, limit, terms, }); } catch (err) { console.error("Search error:", err); return NextResponse.json({ error: "Search failed" }, { status: 500 }); } }