Skip to content

Commit 21bd5a2

Browse files
committed
feat: orama support
1 parent ef1f468 commit 21bd5a2

3 files changed

Lines changed: 165 additions & 28 deletions

File tree

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@deno/gfm": "jsr:@deno/gfm@^0.11.0",
2222
"@pakornv/fresh-plugin-tailwindcss": "jsr:@pakornv/fresh-plugin-tailwindcss@2.0.0-alpha.1",
2323
"@preact/signals": "npm:@preact/signals@^2.0.4",
24+
"@orama/orama": "npm:@orama/orama@3.1.11",
2425
"fresh": "jsr:@fresh/core@^2.0.0-alpha.34",
2526
"lucide-preact": "npm:lucide-preact@^0.513.0",
2627
"preact": "npm:preact@^10.26.6",

deno.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

routes/api/search.ts

Lines changed: 158 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { getTableOfContents, parseFrontmatter } from "../../utils/docs.ts";
2+
import {
3+
create,
4+
insert,
5+
insertMultiple,
6+
search as oramaSearch,
7+
} from "@orama/orama";
28

39
interface SearchResult {
410
title: string;
511
url: string;
612
excerpt: string;
713
type: "doc" | "api" | "example" | "blog" | "std";
8-
// human-friendly label for UI badges (e.g. "Docs", "Std")
914
label: string;
1015
score: number;
1116
highlights: string[];
@@ -30,7 +35,30 @@ interface GitTreeItem {
3035
let indexCache: SearchResult[] | null = null;
3136
let rawIndexCache: RawIndexItem[] | null = null;
3237
let lastIndexTime = 0;
33-
const INDEX_CACHE_DURATION = 60 * 1000; // 60s
38+
const INDEX_CACHE_DURATION = 60 * 1000;
39+
40+
let oramaDb: ReturnType<typeof create> | null = null;
41+
42+
function ensureOrama() {
43+
if (oramaDb) return oramaDb;
44+
try {
45+
oramaDb = create({
46+
schema: {
47+
title: "string",
48+
excerpt: "string",
49+
body: "string",
50+
type: "string",
51+
url: "string",
52+
label: "string",
53+
},
54+
});
55+
return oramaDb;
56+
} catch (e) {
57+
console.warn("Orama init failed, falling back to built-in search:", e);
58+
oramaDb = null;
59+
return null;
60+
}
61+
}
3462

3563
async function buildIndex(): Promise<SearchResult[]> {
3664
const now = Date.now();
@@ -173,6 +201,50 @@ async function buildIndex(): Promise<SearchResult[]> {
173201
rawIndexCache = items;
174202
indexCache = results;
175203
lastIndexTime = Date.now();
204+
205+
// Attempt to index into Orama (best-effort). Use insertMultiple when possible.
206+
try {
207+
const db = ensureOrama();
208+
if (db) {
209+
// Prepare docs for Orama inserting. Keep id as url to make it stable.
210+
const docs = items.map((it) => ({
211+
id: it.url,
212+
title: it.title,
213+
excerpt: it.excerpt || "",
214+
body: it.body || "",
215+
type: it.type,
216+
url: it.url,
217+
label: (() => {
218+
switch (it.type) {
219+
case "api":
220+
return "API";
221+
case "example":
222+
return "Example";
223+
case "blog":
224+
return "Blog";
225+
case "std":
226+
return "Std";
227+
default:
228+
return "Docs";
229+
}
230+
})(),
231+
}));
232+
if (typeof insertMultiple === "function") {
233+
Promise.resolve(insertMultiple(db, docs))
234+
.then(() => {})
235+
.catch((e: Error) => {
236+
console.warn("Orama insertMultiple failed:", e);
237+
});
238+
} else {
239+
for (const d of docs) {
240+
// @ts-ignore: best-effort insert, allow unknown signature
241+
insert(db, d).catch(() => {});
242+
}
243+
}
244+
}
245+
} catch (e) {
246+
console.warn("Failed to populate Orama DB:", e);
247+
}
176248
return results;
177249
}
178250

@@ -232,7 +304,7 @@ function scoreAndFilter(results: SearchResult[], query: string, limit = 10) {
232304

233305
scored.sort((a, b) => b.score - a.score);
234306
return scored.slice(0, limit).map((s) => ({
235-
...s.r,
307+
...s.r,
236308
score: s.score,
237309
highlights: s.highlights,
238310
}));
@@ -279,32 +351,90 @@ export const handler = {
279351
parseInt(url.searchParams.get("pageSize") || String(limit)),
280352
);
281353

282-
const scored = scoreAndFilter(
283-
filtered.map((it) => ({
284-
title: it.title,
285-
url: it.url,
286-
excerpt: it.excerpt || "",
287-
type: it.type,
288-
label: (() => {
289-
switch (it.type) {
290-
case "api":
291-
return "API";
292-
case "example":
293-
return "Example";
294-
case "blog":
295-
return "Blog";
296-
case "std":
297-
return "Std";
298-
default:
299-
return "Docs";
354+
const q = query as string;
355+
356+
let scored: SearchResult[] = [];
357+
try {
358+
const db = oramaDb || ensureOrama();
359+
if (db) {
360+
try {
361+
// @ts-ignore: imported from npm specifier in Deno environment
362+
const oramaRes = await oramaSearch(db, { term: query });
363+
if (
364+
oramaRes &&
365+
Array.isArray((oramaRes as unknown as { hits?: unknown }).hits)
366+
) {
367+
const hits =
368+
(oramaRes as unknown as { hits: Array<unknown> }).hits;
369+
const mapped: SearchResult[] = hits.map((h) => {
370+
const hit = h as unknown as {
371+
id?: string;
372+
score?: number;
373+
document?: Record<string, unknown>;
374+
};
375+
const doc = hit.document || {};
376+
const title = (doc.title as string) || (doc.name as string) ||
377+
"";
378+
const urlVal = (doc.url as string) || hit.id || "";
379+
const excerpt = (doc.excerpt as string) || "";
380+
const typeVal = (doc.type as string) ||
381+
(doc.label as string) || "doc";
382+
const labelVal = (doc.label as string) || "Docs";
383+
return {
384+
title,
385+
url: urlVal,
386+
excerpt,
387+
type: (typeVal as string) as SearchResult["type"],
388+
label: labelVal,
389+
score: typeof hit.score === "number" ? hit.score : 0,
390+
highlights: [],
391+
};
392+
});
393+
394+
const filteredHits = allowedTypes
395+
? mapped.filter((m) => allowedTypes.includes(m.type))
396+
: mapped;
397+
scored = filteredHits;
300398
}
301-
})(),
302-
score: 0,
303-
highlights: [],
304-
})),
305-
query,
306-
filtered.length,
307-
);
399+
} catch (e) {
400+
console.warn("Orama search call failed:", e);
401+
}
402+
}
403+
} catch (e) {
404+
console.warn(
405+
"Orama search failed, falling back to legacy scorer:",
406+
e,
407+
);
408+
}
409+
410+
if (!scored || scored.length === 0) {
411+
scored = scoreAndFilter(
412+
filtered.map((it) => ({
413+
title: it.title,
414+
url: it.url,
415+
excerpt: it.excerpt || "",
416+
type: it.type,
417+
label: (() => {
418+
switch (it.type) {
419+
case "api":
420+
return "API";
421+
case "example":
422+
return "Example";
423+
case "blog":
424+
return "Blog";
425+
case "std":
426+
return "Std";
427+
default:
428+
return "Docs";
429+
}
430+
})(),
431+
score: 0,
432+
highlights: [],
433+
})),
434+
q,
435+
filtered.length,
436+
);
437+
}
308438

309439
const total = scored.length;
310440
const start = (page - 1) * pageSize;

0 commit comments

Comments
 (0)