-
Notifications
You must be signed in to change notification settings - Fork 916
feat: DR-7744 Support page #7699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Mixedbread API Configuration | ||
| # Required for the search functionality to work | ||
| # Get your API key from: https://www.mixedbread.ai/ | ||
| MIXEDBREAD_API_KEY=your_mixedbread_api_key_here | ||
|
|
||
| # Base URL for the site (optional, defaults based on environment) | ||
| # NEXT_PUBLIC_PRISMA_URL=https://www.prisma.io | ||
|
|
||
| # Vercel URL (automatically set in Vercel deployments) | ||
| # VERCEL_URL= | ||
|
|
||
| # Allowed development origins (optional, defaults to localhost,127.0.0.1,192.168.1.48) | ||
| # ALLOWED_DEV_ORIGINS=localhost,127.0.0.1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,158 @@ | ||
| export async function GET() { | ||
| return Response.json( | ||
| { | ||
| error: "Search is not configured in the site host zone.", | ||
| }, | ||
| { status: 404 }, | ||
| ); | ||
| import { SiteSearchResult } from "@/components/support/search-types"; | ||
| import Mixedbread from "@mixedbread/sdk"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| type GeneratedMetadata = { | ||
| title?: string; | ||
| slug?: string; | ||
| metaTitle?: string; | ||
| metaDescription?: string; | ||
| metaImagePath?: string; | ||
| heroImagePath?: string; | ||
| tags?: string[]; | ||
| excerpt?: string; | ||
| url?: string; | ||
| }; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| const mixedbreadApiKey = process.env.MIXEDBREAD_API_KEY; | ||
| const storeIdentifiers = ["blog-search", "web-search"] as const; | ||
| const client = mixedbreadApiKey | ||
| ? new Mixedbread({ apiKey: mixedbreadApiKey }) | ||
| : null; | ||
| const websiteBaseUrl = "https://prisma.io"; | ||
| const blogPrefix = "/blog"; | ||
| const docsPrefix = "/docs"; | ||
|
|
||
| type MixedbreadSearchChunk = { | ||
| id?: string; | ||
| file_id?: string; | ||
| chunk_index?: number; | ||
| text?: string; | ||
| generated_metadata?: GeneratedMetadata; | ||
| }; | ||
|
|
||
| function withPrefixedPath( | ||
| path: string | undefined, | ||
| prefix: string, | ||
| ): string { | ||
| const normalizedPath = (path ?? "") | ||
| .replace(/^\/+/, "") | ||
| .replace(new RegExp(`^${prefix.replace("/", "")}/`), ""); | ||
|
|
||
| return normalizedPath ? `${prefix}/${normalizedPath}` : "#"; | ||
| } | ||
|
|
||
| function withBaseAndPrefixedPath( | ||
| path: string | undefined, | ||
| prefix: string, | ||
| ): string { | ||
| const prefixedPath = withPrefixedPath(path, prefix); | ||
| return prefixedPath === "#" | ||
| ? prefixedPath | ||
| : new URL(prefixedPath, websiteBaseUrl).toString(); | ||
| } | ||
|
|
||
| function normalizeBlogUrl(slug?: string): string { | ||
| return withBaseAndPrefixedPath(slug, blogPrefix); | ||
| } | ||
|
|
||
| function normalizeBlogImagePath(imagePath?: string): string { | ||
| if (!imagePath) return ""; | ||
| if (/^https?:\/\//.test(imagePath)) return imagePath; | ||
| const blogPath = withBaseAndPrefixedPath(imagePath, blogPrefix); | ||
| return blogPath === "#" ? "" : blogPath; | ||
mhartington marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| function normalizeDocsUrl(url?: string): string { | ||
| return withBaseAndPrefixedPath(url, docsPrefix); | ||
| } | ||
|
|
||
| function getDocsImagePath(normalizedDocsUrl: string): string { | ||
| if (!normalizedDocsUrl || normalizedDocsUrl === "#") return ""; | ||
| const docsUrl = new URL(normalizedDocsUrl, websiteBaseUrl); | ||
| const docsPath = docsUrl.pathname.replace(/^\/docs\//, "").replace(/^\/+/, ""); | ||
| return `https://www.prisma.io/docs/og/${docsPath}/image.png`; | ||
| } | ||
|
|
||
| function transformResult(item: MixedbreadSearchChunk): SiteSearchResult | null { | ||
| const metadata = item.generated_metadata; | ||
| if (!metadata) return null; | ||
|
|
||
| const source: SiteSearchResult["source"] | null = metadata.slug | ||
| ? "blog" | ||
| : metadata.url | ||
| ? "docs" | ||
| : null; | ||
| if (!source) return null; | ||
|
|
||
| const base = | ||
| item.id ?? `${item.file_id ?? "unknown"}-${item.chunk_index ?? "0"}`; | ||
|
|
||
| const normalizedUrl = | ||
| source === "blog" | ||
| ? normalizeBlogUrl(metadata.slug) | ||
| : normalizeDocsUrl(metadata.url); | ||
|
|
||
| return { | ||
| id: `${base}-page`, | ||
| type: "page", | ||
| source, | ||
| content: | ||
| source === "blog" | ||
| ? metadata.metaTitle ?? metadata.title ?? item.text ?? "Untitled" | ||
| : metadata.title ?? item.text ?? "Untitled", | ||
| url: normalizedUrl, | ||
| description: | ||
| source === "blog" | ||
| ? metadata.metaDescription ?? metadata.excerpt ?? item.text ?? "" | ||
| : metadata.metaDescription ?? item.text ?? "", | ||
| heroImagePath: | ||
| source === "blog" | ||
| ? normalizeBlogImagePath( | ||
| metadata.heroImagePath ?? metadata.metaImagePath, | ||
| ) | ||
| : getDocsImagePath(normalizedUrl), | ||
| tags: source === "blog" ? metadata.tags ?? [] : [], | ||
| }; | ||
| } | ||
|
|
||
| function transformResults(results: MixedbreadSearchChunk[]): SiteSearchResult[] { | ||
| return results | ||
| .map(transformResult) | ||
| .filter((item): item is SiteSearchResult => item !== null); | ||
| } | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| if (!client) { | ||
| console.error("Search API called but Mixedbread is not configured"); | ||
| console.error("Please set MIXEDBREAD_API_KEY environment variable"); | ||
| return NextResponse.json([]); | ||
| } | ||
|
|
||
| const query = request.nextUrl.searchParams.get("query")?.trim() ?? ""; | ||
|
|
||
| if (!query) { | ||
| return NextResponse.json([]); | ||
| } | ||
|
|
||
| try { | ||
| const response = await client.stores.search({ | ||
| query, | ||
| store_identifiers: [...storeIdentifiers], | ||
| top_k: 20, | ||
| search_options: { | ||
| rerank: true, | ||
| return_metadata: true, | ||
| }, | ||
| }); | ||
|
Comment on lines
+127
to
+149
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Put abuse protection in front of the Mixedbread call. This is a public GET endpoint and every non-empty query is forwarded straight to 🤖 Prompt for AI Agents |
||
|
|
||
| return NextResponse.json( | ||
| transformResults(response.data as MixedbreadSearchChunk[]), | ||
| ); | ||
| } catch (error) { | ||
| console.error("Mixedbread search failed:", error); | ||
| return NextResponse.json([]); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.