basic search

This commit is contained in:
Mariano Gabriel
2026-07-05 22:00:52 -03:00
parent 8ff29fc776
commit b65d6075be
4 changed files with 143 additions and 15 deletions

View File

@@ -73,6 +73,53 @@ export function doocusApi(opts: Options): Plugin {
return abs
}
// Lazy search index over the cached small text — rebuilt when index.json changes.
let searchCache: { mtime: number; entries: Array<{ path: string; hay: string; bytes: number }> } | null = null
// Read full text for reliable search (local files; speed is secondary). The
// generous cap only guards against a pathologically huge file.
async function readCap(p: string | null, cap = 20_000_000): Promise<string> {
if (!p) return ''
try { return (await fsp.readFile(p, 'utf-8')).slice(0, cap) } catch { return '' }
}
async function buildSearchIndex(): Promise<void> {
const index = readIndex()
if (!index) { searchCache = { mtime: 0, entries: [] }; return }
let mtime = 0
try { mtime = fs.statSync(indexPath).mtimeMs } catch { /* */ }
const entries: Array<{ path: string; hay: string; bytes: number }> = []
for (const f of index.files) {
let text = `${f.path} ${(f as any).title ?? ''}`
if (f.mode === 'extracted' && f.out) {
text += ' ' + await readCap(path.join(outputDir, f.out, 'content.md'))
} else if (f.mode === 'meeting') {
const { dir, stem } = meetusDirFor(f.path)
text += ' ' + await readCap(path.join(dir, `${stem}_enhanced.txt`))
} else if (TEXT_EXTS.has(f.ext)) {
text += ' ' + await readCap(originalAbs(index, f.path))
}
entries.push({ path: f.path, hay: text.toLowerCase(), bytes: f.bytes })
}
searchCache = { mtime, entries }
}
async function search(res: ServerResponse, query: string): Promise<void> {
const q = query.trim().toLowerCase()
let curMtime = 0
try { curMtime = fs.statSync(indexPath).mtimeMs } catch { /* */ }
if (!searchCache || searchCache.mtime !== curMtime) await buildSearchIndex()
const entries = searchCache?.entries ?? []
// Empty query matches nothing (the UI shows the full tree when not searching).
const hits = q ? entries.filter((e) => e.hay.includes(q)) : []
sendJson(res, 200, {
query: q,
matches: hits.map((e) => e.path),
count: hits.length,
bytes: hits.reduce((s, e) => s + e.bytes, 0),
})
}
return {
name: 'doocus-api',
configureServer(server) {
@@ -118,6 +165,11 @@ export function doocusApi(opts: Options): Plugin {
return true
}
if (parts[0] === 'search') {
await search(res, q.get('q') ?? '')
return true
}
// meetus-shaped run API (id = meeting rel path), so the composed meetus
// review store works unchanged when embedded in doocus.
if (parts[0] === 'runs' && parts.length >= 2) {