Add embedded Tantivy full-text content search
/api/search now finds files by CONTENT as well as by name: BM25-ranked
matches over extracted text (PDF, Office OOXML/ODF, plain text/code)
with typo-tolerant fuzzy terms and search-as-you-type prefix matching,
served from an embedded Tantivy index at {storage}/.search-index.
Pipeline (all off the request path, mirroring tree-etag + thumbnails):
- statement triggers on storage.files append to a durable dirty queue
(storage.search_index_dirty) - every write surface (REST, WebDAV,
NextCloud, WOPI, trash) is covered, crash-safe by construction
- ContentIndexWorker drains the queue on the maintenance pool, extracts
text once per unique BLAKE3 blob (storage.blob_extracted_text cache:
N copies = 1 extraction, renames/moves = 0 re-extraction) and applies
batched single-writer Tantivy commits; queue rows are deleted only
after the commit succeeds (at-least-once, idempotent upserts)
- the index is a derived artifact: a version-marker mismatch wipes and
reseeds it from Postgres, which remains the single source of truth
SearchService merges content hits into the existing name search: hits
are hydrated through ONE SQL round-trip that re-applies user scope,
trash state and every active filter (a stale index id can never leak),
scored below name matches, and returned with a plain-text snippet and
a match_source field. Index failure or
OXICLOUD_ENABLE_CONTENT_SEARCH=false degrades to name-only search; a
discard-only janitor keeps the trigger-fed queue bounded while disabled.
The frontend renders the snippet under the file name in list view.
New dependencies: tantivy 0.26, zip 8.6 (deflate only), pdf-extract 0.10.
https://claude.ai/code/session_01Sc7F4xbo83YbFAQ4xEeDrX
This commit is contained in:
@@ -269,6 +269,26 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Content-search snippet — fragment of the file body that matched the
|
||||
query. Wraps onto a second line inside the flex name-cell. */
|
||||
.files-list-view .file-item .name-cell:has(.file-item__snippet) {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-item__snippet {
|
||||
flex-basis: 100%;
|
||||
/* icon width (36px) + cell gap (12px) — aligns under the name */
|
||||
padding-left: 48px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Grid cards are too compact for body fragments. */
|
||||
.files-grid-view .file-item .file-item__snippet {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.files-list-view .file-item .file-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
Reference in New Issue
Block a user