route.ts 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. import { NextRequest, NextResponse } from "next/server";
  2. import { getSearchIndex } from "@/lib/search-index";
  3. export async function GET(req: NextRequest) {
  4. const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
  5. if (!q) {
  6. return NextResponse.json({ results: [] });
  7. }
  8. const index = await getSearchIndex();
  9. const terms = q.split(/\s+/).filter(Boolean);
  10. const results = index
  11. .map((entry) => {
  12. const titleLower = entry.title.toLowerCase();
  13. const contentLower = entry.content.toLowerCase();
  14. const titleMatch = terms.every((t) => titleLower.includes(t));
  15. const contentMatch = terms.every((t) => contentLower.includes(t));
  16. if (!titleMatch && !contentMatch) return null;
  17. let snippet = "";
  18. if (contentMatch) {
  19. const firstTermIdx = Math.min(
  20. ...terms.map((t) => {
  21. const idx = contentLower.indexOf(t);
  22. return idx === -1 ? Infinity : idx;
  23. }),
  24. );
  25. if (firstTermIdx !== Infinity) {
  26. const start = Math.max(0, firstTermIdx - 40);
  27. const end = Math.min(entry.content.length, firstTermIdx + 120);
  28. snippet =
  29. (start > 0 ? "..." : "") +
  30. entry.content.slice(start, end).replace(/\n/g, " ") +
  31. (end < entry.content.length ? "..." : "");
  32. }
  33. }
  34. return {
  35. title: entry.title,
  36. href: entry.href,
  37. section: entry.section,
  38. snippet,
  39. score: titleMatch ? 2 : 1,
  40. };
  41. })
  42. .filter(
  43. (
  44. r,
  45. ): r is {
  46. title: string;
  47. href: string;
  48. section: string;
  49. snippet: string;
  50. score: number;
  51. } => r !== null,
  52. )
  53. .sort((a, b) => b.score - a.score)
  54. .slice(0, 20)
  55. .map(({ title, href, section, snippet }) => ({
  56. title,
  57. href,
  58. section,
  59. snippet,
  60. }));
  61. return NextResponse.json(
  62. { results },
  63. { headers: { "Cache-Control": "public, max-age=60" } },
  64. );
  65. }