Stohr ships two search surfaces:
- Filename search (
GET /search) — pg_trgm similarity overfiles.nameandfolders.name, plus filter tokens (kind:,ext:). Always on; no extraction. - Full-text content search (
GET /search/content) — Postgrestsvectorover extracted file text, withts_headlinesnippets. Indexing happens out-of-band so uploads stay fast.
Both searches restrict results to resources the caller can read in their team, including Space membership.
Use Search files, folders, and contents in the file toolbar or press ⌘K / Ctrl+K. Name search includes files and folders; content search finds text inside supported documents. Open a folder result to browse it, or a file result to preview it. Empty results, loading, and retryable failures are shown separately.
The file browser has a Grid / List / Gallery switch. Gallery keeps documents visible alongside images and videos, with filenames beneath previews. The selected view is remembered in that browser; it does not change anyone else's view.
The background indexer wakes every 30 seconds, claims the next 5 files that haven't been indexed against their current version, and extracts text:
| Type | How |
|---|---|
text/*, application/json, source code, log files |
Read as UTF-8 |
text/html, *.html |
Tag-stripped |
application/pdf |
pdftotext -enc UTF-8 -nopgbrk - - (poppler-utils) |
*.docx, *.xlsx, *.pptx |
unzip -p + XML extraction |
| Anything else (images, video, archives) | Skipped, recorded as text_extract_error = 'skipped: …' |
If pdftotext / unzip are not installed on the host, those formats are skipped gracefully — Stohr keeps working and the indexer just records the missing-tool reason in files.text_extract_error. Install poppler-utils and unzip on the host (both are tiny) to cover PDF and Office docs.
Extracted text is capped at 1 MiB per file. Files over 50 MiB are skipped before extraction. External extractors are terminated after 8 seconds and have a 2 MiB output ceiling. With malware scanning enabled, only clean files are indexed and only clean files produce content snippets; see Configuration.
The indexed files.text_tsv is a GENERATED column combining the filename (weight A) and the extracted content (weight B), so renames and re-uploads automatically update the index.
/search/content?q=… runs the query through Postgres websearch_to_tsquery, which supports:
- Plain keywords:
migration plan - Quoted phrases:
"key rotation" - Boolean OR:
migration OR rollout - Negation:
migration -draft
Each hit returns the file row plus a snippet. The server HTML-escapes document text and adds <mark> only around matched terms. Preserve that escaping if rendering the snippet as HTML; never substitute raw extracted text.
GET /admin/content-index/status (owner-only) returns:
{
"total": 1240,
"indexed": 1232,
"pending": 3,
"errored": 5
}A new import can create a backlog larger than one batch. Check that pending falls across successive ticks. errored only counts rows where extraction returned an actual error (not "skipped").
- Indexer batch size lives in
src/server.ts(indexContentBatch(db, store, 5)). Bump if your backlog is large; each batch holds files in memory while extracting, so don't go higher than ~20. - Index frequency is
setInterval(…, 30 * 1000). The cron is guarded so a slow run can't stack. - Max indexed text per file is
MAX_TEXT_BYTESinsrc/search/content/extract.ts. Bumping it past a few MiB starts to makets_headlineslow.
- Image/video/audio content (no OCR / ASR — out of scope for now)
- Encrypted archives (zip with password, rar) —
unzipcan't open them - Files in
/trash(soft-deleted) - Federation blobs you don't own a local copy of