e7738de6d2
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getSearchIndex } from "@/lib/search-index";
|
|
import type { SearchResult } from "@/lib/types";
|
|
|
|
type ScoredSearchResult = SearchResult & { score: number };
|
|
|
|
function isScoredResult(result: ScoredSearchResult | null): result is ScoredSearchResult {
|
|
return result !== null;
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
|
|
if (!q) return NextResponse.json({ results: [] });
|
|
|
|
const index = await getSearchIndex();
|
|
const terms = q.split(/\s+/).filter(Boolean);
|
|
|
|
const results = index
|
|
.map((entry) => {
|
|
const titleLower = entry.title.toLowerCase();
|
|
const contentLower = entry.content.toLowerCase();
|
|
|
|
const titleMatch = terms.every((t) => titleLower.includes(t));
|
|
const contentMatch = terms.every((t) => contentLower.includes(t));
|
|
|
|
if (!titleMatch && !contentMatch) return null;
|
|
|
|
let snippet = "";
|
|
if (contentMatch) {
|
|
const firstTermIdx = Math.min(
|
|
...terms.map((t) => {
|
|
const idx = contentLower.indexOf(t);
|
|
return idx === -1 ? Infinity : idx;
|
|
}),
|
|
);
|
|
if (firstTermIdx !== Infinity) {
|
|
const start = Math.max(0, firstTermIdx - 40);
|
|
const end = Math.min(entry.content.length, firstTermIdx + 120);
|
|
snippet =
|
|
(start > 0 ? "..." : "") +
|
|
entry.content.slice(start, end).replace(/\n/g, " ") +
|
|
(end < entry.content.length ? "..." : "");
|
|
}
|
|
}
|
|
|
|
return {
|
|
title: entry.title,
|
|
href: entry.href,
|
|
section: entry.section,
|
|
snippet,
|
|
score: titleMatch ? 2 : 1,
|
|
};
|
|
})
|
|
.filter(isScoredResult)
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, 20)
|
|
.map(({ score: _, ...rest }) => rest);
|
|
|
|
return NextResponse.json({ results }, { headers: { "Cache-Control": "public, max-age=60" } });
|
|
}
|