Core: Add orama-cloud-legacy search integration for old Orama Cloud users

This commit is contained in:
Fuma Nama
2026-01-03 14:29:22 +08:00
parent 7b71dc1c4e
commit 7e08b2f8a9
8 changed files with 345 additions and 34 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"fumadocs-core": patch
---
Add `orama-cloud-legacy` search integration for old Orama Cloud users
+5
View File
@@ -143,6 +143,7 @@
"@mdx-js/mdx": "^3.1.1",
"@mixedbread/sdk": "^0.46.0",
"@orama/core": "^1.2.16",
"@oramacloud/client": "^2.1.4",
"@tanstack/react-router": "1.144.0",
"@types/estree-jsx": "^1.0.5",
"@types/hast": "^3.0.4",
@@ -172,6 +173,7 @@
"peerDependencies": {
"@mixedbread/sdk": "^0.46.0",
"@orama/core": "1.x.x",
"@oramacloud/client": "2.x.x",
"@tanstack/react-router": "1.x.x",
"@types/react": "*",
"algoliasearch": "5.x.x",
@@ -193,6 +195,9 @@
"@orama/core": {
"optional": true
},
"@oramacloud/client": {
"optional": true
},
"algoliasearch": {
"optional": true
},
+31 -26
View File
@@ -5,6 +5,7 @@ import { useOnChange } from '@/utils/use-on-change';
import { type StaticOptions } from '@/search/client/static';
import { type AlgoliaOptions } from '@/search/client/algolia';
import { type OramaCloudOptions } from '@/search/client/orama-cloud';
import { type OramaCloudLegacyOptions } from '@/search/client/orama-cloud-legacy';
import { type MixedbreadOptions } from '@/search/client/mixedbread';
import type { SortedResult } from '@/search';
@@ -31,6 +32,9 @@ export type Client =
| ({
type: 'orama-cloud';
} & OramaCloudOptions)
| ({
type: 'orama-cloud-legacy';
} & OramaCloudLegacyOptions)
| ({
type: 'mixedbread';
} & MixedbreadOptions);
@@ -106,33 +110,34 @@ export function useDocsSearch(
async function run(): Promise<SortedResult[] | 'empty'> {
if (debouncedValue.length === 0 && !allowEmpty) return 'empty';
if (client.type === 'fetch') {
const { fetchDocs } = await import('./client/fetch');
return fetchDocs(debouncedValue, client);
switch (client.type) {
case 'fetch': {
const { fetchDocs } = await import('./client/fetch');
return fetchDocs(debouncedValue, client);
}
case 'algolia': {
const { searchDocs } = await import('./client/algolia');
return searchDocs(debouncedValue, client);
}
case 'orama-cloud': {
const { searchDocs } = await import('./client/orama-cloud');
return searchDocs(debouncedValue, client);
}
case 'orama-cloud-legacy': {
const { searchDocs } = await import('./client/orama-cloud-legacy');
return searchDocs(debouncedValue, client);
}
case 'mixedbread': {
const { search } = await import('./client/mixedbread');
return search(debouncedValue, client);
}
case 'static': {
const { search } = await import('./client/static');
return search(debouncedValue, client);
}
default:
throw new Error('unknown search client');
}
if (client.type === 'algolia') {
const { searchDocs } = await import('./client/algolia');
return searchDocs(debouncedValue, client);
}
if (client.type === 'orama-cloud') {
const { searchDocs } = await import('./client/orama-cloud');
return searchDocs(debouncedValue, client);
}
if (client.type === 'static') {
const { search } = await import('./client/static');
return search(debouncedValue, client);
}
if (client.type === 'mixedbread') {
const { search } = await import('./client/mixedbread');
return search(debouncedValue, client);
}
throw new Error('unknown search client');
}
void run()
@@ -0,0 +1,129 @@
import type { ClientSearchParams, OramaClient } from '@oramacloud/client';
import { removeUndefined } from '@/utils/remove-undefined';
import type { OramaIndex } from '@/search/orama-cloud-legacy';
import { createContentHighlighter, type SortedResult } from '@/search';
interface CrawlerIndex {
path: string;
title: string;
content: string;
section: string;
category: string;
}
export interface OramaCloudLegacyOptions {
client: OramaClient;
/**
* The type of your index.
*
* You can set it to `crawler` if you use crawler instead of the JSON index with schema provided by Fumadocs
*/
index?: 'default' | 'crawler';
params?: ClientSearchParams;
/**
* Filter results with specific tag.
*/
tag?: string;
/**
* Filter by locale (unsupported at the moment)
*/
locale?: string;
}
export async function searchDocs(
query: string,
options: OramaCloudLegacyOptions,
): Promise<SortedResult[]> {
const highlighter = createContentHighlighter(query);
const list: SortedResult[] = [];
const { index = 'default', client, params: extraParams = {}, tag } = options;
if (index === 'crawler') {
const result = await client.search({
...extraParams,
term: query,
where: {
category: tag
? {
eq: tag.slice(0, 1).toUpperCase() + tag.slice(1),
}
: undefined,
...extraParams.where,
},
limit: 10,
});
if (!result) return list;
for (const hit of result.hits) {
const doc = hit.document as unknown as CrawlerIndex;
list.push(
{
id: hit.id,
type: 'page',
content: doc.title,
contentWithHighlights: highlighter.highlight(doc.title),
url: doc.path,
},
{
id: 'page' + hit.id,
type: 'text',
content: doc.content,
contentWithHighlights: highlighter.highlight(doc.content),
url: doc.path,
},
);
}
return list;
}
const params: ClientSearchParams = {
...extraParams,
term: query,
where: removeUndefined({
tag,
...extraParams.where,
}),
groupBy: {
properties: ['page_id'],
maxResult: 7,
...extraParams.groupBy,
},
};
const result = await client.search(params);
if (!result || !result.groups) return list;
for (const item of result.groups) {
let addedHead = false;
for (const hit of item.result) {
const doc = hit.document as unknown as OramaIndex;
if (!addedHead) {
list.push({
id: doc.page_id,
type: 'page',
content: doc.title,
breadcrumbs: doc.breadcrumbs,
contentWithHighlights: highlighter.highlight(doc.title),
url: doc.url,
});
addedHead = true;
}
list.push({
id: doc.id,
content: doc.content,
contentWithHighlights: highlighter.highlight(doc.content),
type: doc.content === doc.section ? 'heading' : 'text',
url: doc.section_id ? `${doc.url}#${doc.section_id}` : doc.url,
});
}
}
return list;
}
@@ -0,0 +1,154 @@
import type { CloudManager } from '@oramacloud/client';
import type { StructuredData } from '@/mdx-plugins';
export interface SyncOptions {
/**
* Index name to sync
*/
index: string;
documents: OramaDocument[];
/**
* Deploy changes
*
* @defaultValue true
*/
autoDeploy?: boolean;
}
export interface I18nSyncOptions extends Omit<SyncOptions, 'index' | 'documents'> {
/**
* Indexes to sync.
*
* Pairs of `locale`-`index`.
**/
indexes: Record<string, string>;
documents: {
locale: string;
items: OramaDocument[];
}[];
}
export interface OramaDocument {
/**
* The ID of document, must be unique
*/
id: string;
title: string;
description?: string;
/**
* URL to the page
*/
url: string;
structured: StructuredData;
/**
* Tag to filter results
*/
tag?: string;
/**
* Data to be added to each section index
*/
extra_data?: object;
breadcrumbs?: string[];
}
export interface OramaIndex {
id: string;
title: string;
url: string;
tag?: string;
/**
* The id of page, used for `group by`
*/
page_id: string;
/**
* Heading content
*/
section?: string;
breadcrumbs?: string[];
/**
* Heading (anchor) id
*/
section_id?: string;
content: string;
}
export async function sync(cloudManager: CloudManager, options: SyncOptions): Promise<void> {
const { autoDeploy = true } = options;
const index = cloudManager.index(options.index);
await index.snapshot(options.documents.flatMap(toIndex));
if (autoDeploy) await index.deploy();
}
export async function syncI18n(
cloudManager: CloudManager,
options: I18nSyncOptions,
): Promise<void> {
const { autoDeploy = true } = options;
const tasks = options.documents.map(async (document) => {
const index = cloudManager.index(options.indexes[document.locale]);
await index.snapshot(document.items.flatMap(toIndex));
if (autoDeploy) await index.deploy();
});
await Promise.all(tasks);
}
function toIndex(page: OramaDocument): OramaIndex[] {
let id = 0;
const indexes: OramaIndex[] = [];
const scannedHeadings = new Set<string>();
function createIndex(
section: string | undefined,
sectionId: string | undefined,
content: string,
): OramaIndex {
return {
id: `${page.id}-${(id++).toString()}`,
title: page.title,
url: page.url,
page_id: page.id,
tag: page.tag,
section,
section_id: sectionId,
content,
breadcrumbs: page.breadcrumbs,
...page.extra_data,
};
}
if (page.description) indexes.push(createIndex(undefined, undefined, page.description));
page.structured.contents.forEach((p) => {
const heading = p.heading ? page.structured.headings.find((h) => p.heading === h.id) : null;
const index = createIndex(heading?.content, heading?.id, p.content);
if (heading && !scannedHeadings.has(heading.id)) {
scannedHeadings.add(heading.id);
indexes.push(createIndex(heading.content, heading.id, heading.content));
}
indexes.push(index);
});
return indexes;
}
+2 -2
View File
@@ -17,7 +17,7 @@ export interface SyncOptions {
autoDeploy?: boolean;
}
export type I18nSyncOptions = Omit<SyncOptions, 'index' | 'documents'> & {
export interface I18nSyncOptions extends Omit<SyncOptions, 'index' | 'documents'> {
/**
* Indexes to sync.
*
@@ -29,7 +29,7 @@ export type I18nSyncOptions = Omit<SyncOptions, 'index' | 'documents'> & {
locale: string;
items: OramaDocument[];
}[];
};
}
export interface OramaDocument {
/**
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
'src/source/{index,schema}.ts',
'src/source/client/*.{ts,tsx}',
'src/source/plugins/{lucide-icons,slugs}.ts',
'src/search/{index,client,server,algolia,orama-cloud}.ts',
'src/search/{index,client,server,algolia,orama-cloud,orama-cloud-legacy}.ts',
'src/utils/use-on-change.ts',
'src/utils/use-effect-event.ts',
'src/utils/use-media-query.ts',
+18 -5
View File
@@ -945,7 +945,7 @@ importers:
version: 19.2.3(@types/react@19.2.7)
'@vitejs/plugin-rsc':
specifier: ^0.5.7
version: 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
version: 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
react-router-devtools:
specifier: ^6.0.1
version: 6.0.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react-router@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -1593,6 +1593,9 @@ importers:
'@orama/core':
specifier: ^1.2.16
version: 1.2.16
'@oramacloud/client':
specifier: ^2.1.4
version: 2.1.4
'@tanstack/react-router':
specifier: 1.144.0
version: 1.144.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -2161,7 +2164,7 @@ importers:
version: 5.1.2(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@vitejs/plugin-rsc':
specifier: ^0.5.7
version: 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
version: 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
commander:
specifier: ^14.0.2
version: 14.0.2
@@ -4032,6 +4035,9 @@ packages:
resolution: {integrity: sha512-Ra4dFddWZ7hCGPAehnd/6QZjlzQvczYSt1y1Fq4HteJqRu2yvfR6fXvxiUnKi+HIgJYPxKhebJEjvJdF8LYQWg==}
engines: {node: '>= 20.0.0'}
'@oramacloud/client@2.1.4':
resolution: {integrity: sha512-uNPFs4wq/iOPbggCwTkVNbIr64Vfd7ZS/h+cricXVnzXWocjDTfJ3wLL4lr0qiSu41g8z+eCAGBqJ30RO2O4AA==}
'@oslojs/encoding@1.1.0':
resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
@@ -6535,6 +6541,7 @@ packages:
bun@1.3.5:
resolution: {integrity: sha512-c1YHIGUfgvYPJmLug5QiLzNWlX2Dg7X/67JWu1Va+AmMXNXzC/KQn2lgQ7rD+n1u1UqDpJMowVGGxTNpbPydNw==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
@@ -13051,6 +13058,12 @@ snapshots:
dependencies:
'@orama/orama': 3.1.18
'@oramacloud/client@2.1.4':
dependencies:
'@orama/cuid2': 2.2.3
'@orama/orama': 3.1.18
lodash: 4.17.21
'@oslojs/encoding@1.1.0': {}
'@oven/bun-darwin-aarch64@1.3.5':
@@ -13769,7 +13782,7 @@ snapshots:
vite-node: 3.2.4(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
optionalDependencies:
'@react-router/serve': 7.11.0(react-router@7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)
'@vitejs/plugin-rsc': 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@vitejs/plugin-rsc': 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
react-server-dom-webpack: 19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1)
typescript: 5.9.3
transitivePeerDependencies:
@@ -15226,7 +15239,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-rsc@0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
'@vitejs/plugin-rsc@0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
dependencies:
es-module-lexer: 2.0.0
estree-walker: 3.0.3
@@ -21283,7 +21296,7 @@ snapshots:
dependencies:
'@hono/node-server': 1.19.7(hono@4.11.3)
'@vitejs/plugin-react': 5.1.2(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@vitejs/plugin-rsc': 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
'@vitejs/plugin-rsc': 0.5.9(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vite@7.3.0(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
dotenv: 17.2.3
hono: 4.11.3
magic-string: 0.30.21