Core: Support mode option in search server

This commit is contained in:
Fuma Nama
2025-09-13 19:32:20 +08:00
parent 397c614d15
commit 2b303151fb
8 changed files with 104 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Support `mode` option in search server
+5 -1
View File
@@ -30,6 +30,7 @@ import { Installation } from '@/components/preview/installation';
import { Customisation } from '@/components/preview/customisation';
import { DocsPage } from 'fumadocs-ui/page';
import { NotFound } from '@/components/not-found';
import { getSuggestions } from '@/app/docs/[...slug]/suggestions';
function PreviewRenderer({ preview }: { preview: string }): ReactNode {
if (preview && preview in Preview) {
@@ -48,7 +49,10 @@ export default async function Page(props: PageProps<'/docs/[...slug]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) return <NotFound slug={params.slug} />;
if (!page)
return (
<NotFound getSuggestions={() => getSuggestions(params.slug.join(' '))} />
);
const preview = page.data.preview;
const { body: Mdx, toc, lastModified } = page.data;
@@ -0,0 +1,30 @@
import { OramaClient } from '@oramacloud/client';
import type { Suggestion } from '@/components/not-found';
const client = new OramaClient({
endpoint: 'https://cloud.orama.run/v1/indexes/docs-fk97oe',
api_key: 'oPZjdlFbq5BpR54bV5Vj57RYt83Xosk7',
});
export async function getSuggestions(pathname: string): Promise<Suggestion[]> {
const results = await client.search({
term: pathname,
mode: 'vector',
groupBy: {
properties: ['url'],
maxResult: 1,
},
});
if (!results?.groups) return [];
return results.groups.map((group) => {
const doc = group.result[0];
return {
id: doc.id,
href: doc.document.url,
title: doc.document.title,
};
});
}
+32 -35
View File
@@ -1,43 +1,44 @@
import { OramaClient } from '@oramacloud/client';
import Link from 'fumadocs-core/link';
import { Suspense } from 'react';
import { type ReactNode, Suspense } from 'react';
import { cn } from '@/lib/cn';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
const client = new OramaClient({
endpoint: 'https://cloud.orama.run/v1/indexes/docs-fk97oe',
api_key: 'oPZjdlFbq5BpR54bV5Vj57RYt83Xosk7',
});
export interface Suggestion {
id: string;
href: string;
title: ReactNode;
}
export function NotFound({ slug }: { slug: string[] }) {
export interface NotFoundProps {
getSuggestions: () => Promise<Suggestion[]>;
}
export function NotFound(props: NotFoundProps) {
return (
<div className="flex flex-col items-center justify-center text-center gap-4 flex-1 p-8 me-(--fd-toc-width)">
<h1 className="text-4xl font-bold font-mono">Not Found</h1>
<div className="w-full border border-fd-foreground/50 border-dashed p-4 max-w-[400px]">
<Suspense
fallback={<p className="font-medium">Finding Alternatives...</p>}
fallback={
<p className="text-sm text-fd-muted-foreground">
Finding Alternatives...
</p>
}
>
<Alternative term={slug.join(' ')} />
<Alternative {...props} />
</Suspense>
</div>
</div>
);
}
async function Alternative({ term }: { term: string }) {
const result = await client.search({
term,
mode: 'vector',
groupBy: {
properties: ['url'],
maxResult: 1,
},
});
async function Alternative({ getSuggestions }: NotFoundProps) {
const suggestions = await getSuggestions();
if (!result.groups || result.groups.length === 0) {
if (suggestions.length === 0) {
return (
<div>
<p className="font-medium mb-2">No Alternative Found</p>
<p className="mb-2">No Alternative Found</p>
<Link href="/" className={cn(buttonVariants({ variant: 'secondary' }))}>
Return to Home
</Link>
@@ -47,23 +48,19 @@ async function Alternative({ term }: { term: string }) {
return (
<div>
<h2 className="font-medium mb-4">Maybe you are looking for</h2>
<h2 className="mb-4">Maybe you are looking for</h2>
<div className="flex flex-col gap-2">
{result.groups.map((group) => {
const doc = group.result[0];
return (
<Link
key={doc.id}
href={doc.document.url}
className="block text-sm p-3 rounded-lg border bg-fd-card text-fd-card-foreground shadow-md transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
<p className="font-medium">{doc.document.title}</p>
<p className="text-fd-muted-foreground">{doc.document.url}</p>
</Link>
);
})}
{suggestions.map((doc) => (
<Link
key={doc.id}
href={doc.href}
className="block text-sm p-3 rounded-lg border bg-fd-card text-fd-card-foreground shadow-md transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
>
<p className="font-medium">{doc.title}</p>
<p className="text-fd-muted-foreground">{doc.href}</p>
</Link>
))}
</div>
</div>
);
@@ -14,6 +14,7 @@ export const advancedSchema = {
type: 'string',
tags: 'enum[]',
url: 'string',
embeddings: 'vector[512]',
} as const;
export async function createDB({
@@ -17,6 +17,7 @@ export function createEndpoint(server: SearchServer): SearchAPI {
await search(query, {
tag: url.searchParams.get('tag')?.split(',') ?? undefined,
locale: url.searchParams.get('locale') ?? undefined,
mode: url.searchParams.get('mode') === 'vector' ? 'vector' : 'full',
}),
);
},
@@ -11,14 +11,16 @@ export async function searchAdvanced(
db: Orama<typeof advancedSchema>,
query: string,
tag: string | string[] = [],
extraParams: Partial<
SearchParams<Orama<typeof advancedSchema>, AdvancedDocument>
> = {},
{
mode = 'fulltext',
...override
}: Partial<SearchParams<Orama<typeof advancedSchema>, AdvancedDocument>> = {},
): Promise<SortedResult[]> {
if (typeof tag === 'string') tag = [tag];
let params = {
...extraParams,
...override,
mode,
where: removeUndefined({
tags:
tag.length > 0
@@ -26,12 +28,12 @@ export async function searchAdvanced(
containsAll: tag,
}
: undefined,
...extraParams.where,
...override.where,
}),
groupBy: {
properties: ['page_id'],
maxResult: 8,
...extraParams.groupBy,
...override.groupBy,
},
} as SearchParams<typeof db, AdvancedDocument>;
@@ -39,7 +41,7 @@ export async function searchAdvanced(
params = {
...params,
term: query,
properties: ['content'],
properties: mode === 'fulltext' ? ['content'] : ['content', 'embeddings'],
} as SearchParams<typeof db, AdvancedDocument>;
}
+21 -2
View File
@@ -31,7 +31,11 @@ export type ExportedData =
export interface SearchServer {
search: (
query: string,
options?: { locale?: string; tag?: string | string[] },
options?: {
locale?: string;
tag?: string | string[];
mode?: 'vector' | 'full';
},
) => Promise<SortedResult[]>;
/**
@@ -154,8 +158,23 @@ export function initAdvancedSearch(options: AdvancedOptions): SearchServer {
},
async search(query, searchOptions) {
const db = await get;
const mode = searchOptions?.mode;
return searchAdvanced(db, query, searchOptions?.tag, options.search);
return searchAdvanced(db, query, searchOptions?.tag, {
...options.search,
mode: mode === 'vector' ? 'vector' : 'fulltext',
}).catch((err) => {
if (mode === 'vector') {
throw new Error(
'failed to search, make sure you have installed `@orama/plugin-embeddings` according to their docs.',
{
cause: err,
},
);
}
throw err;
});
},
};
}