basic search
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user