Summary
The document search APIs (GET /api/doc and GET /api/v1/documents) only match against doc.title using SQL LIKE/ILIKE. Host applications (e.g. RoleWeave's "Memory & Collaboration" panel) that embed doc as a read-only knowledge source cannot search document content, filter by time range, or sort by update/create date with any granularity beyond the default updatedAt desc. When a workspace accumulates 30+ documents and the target keyword only appears in the body, the search returns nothing.
This issue tracks upgrading the doc-side search infrastructure to support multi-dimensional indexed retrieval. The host-app UI layer is out of scope for this repo; this issue focuses on the API and database capabilities that host apps consume.
Current behavior (code evidence)
Internal API: GET /api/doc
- File:
src/app/api/doc/route.ts (lines 231-234)
- Search is title-only:
whereOpt.title = { contains: keyword } (case-sensitive LIKE)
- No pagination — returns all matching rows
- No content search, no time/metadata filters
API v1: GET /api/v1/documents
- File:
src/lib/api-v1-documents.ts (lines 159-204)
- Search is title-only:
{ title: { contains: query, mode: 'insensitive' } } (case-insensitive ILIKE)
- Has cursor-based pagination by
(updatedAt, id) — good foundation
- Query param
query is the only search dimension; starred and trash are the only filters
- No content search, no time range, no metadata filters
Prisma schema: Doc model
- File:
prisma/schema.prisma (lines 19-41)
- Has
content: String (TipTap JSON) — available for indexing but not searched
- Has
contentBinary: Bytes? (Yjs binary state)
- Has
createdAt: DateTime and updatedAt: DateTime — available for range queries but not exposed as filters
- No file type / MIME type field
- No file size field
- No tsvector column or GIN index
Full-text search infrastructure
- None exists. No
tsvector, no GIN index, no @@ operator, no search migration.
Read-only / shared access
- File:
src/lib/document-access.ts, prisma/schema.prisma (ShareRelation model)
- ShareRelation supports
READ / WRITE access types
- API v1
getApiDocument resolves access as owner / write / read
- Host apps consuming docs via PAT with
documents:read scope already see shared documents — search just needs to respect the same userId scope
Expected behavior
Phase 1 — Content full-text search (core)
- Add a
contentSearch tsvector column (or equivalent) to the Doc table, populated from the plain-text extraction of content (TipTap JSON → text).
- Create a GIN index on the tsvector column.
- Extend
GET /api/v1/documents query param query to search both title and document content:
query=keyword → matches title OR content containing "keyword"
- Return a
matchField indicator in results (title | content | both) so the host UI can highlight where the match was found
- Extend internal
GET /api/doc keyword param with the same title+content search.
- Keep the existing
contains fallback for environments where PostgreSQL full-text search is not configured (e.g. SQLite dev).
Phase 2 — Time range filters
- Add query params to
GET /api/v1/documents:
after=2026-09-01 — documents updated after this ISO date
before=2026-09-17 — documents updated before this ISO date
sort=updated_asc|updated_desc|created_asc|created_desc (default: updated_desc)
- Same params on internal
GET /api/doc.
Phase 3 — Metadata filters
- Add a
mimeType or docType field to the Doc model (or a derived classification):
- TipTap documents →
application/tiptap
- Future file attachments → their actual MIME types
- Add query param
type=tiptap|markdown|json|... to filter by document type.
- Add
minSize / maxSize query params for content size filtering.
Phase 4 — Structured query syntax (optional, host-UI side)
- The host app UI can parse simple query syntax like
type:json after:2026-09-01 keyword and translate it into the API query params above. This is a host-app concern; doc just needs to provide the filter params.
Suggested technical approach
- PostgreSQL
tsvector + GIN is the natural fit since the stack is already Prisma + PostgreSQL.
- Use a Prisma migration to add the column and index.
- Populate
contentSearch via a trigger on INSERT/UPDATE of the content column, or via application-level extraction in the create/update path.
- For TipTap JSON → plain text extraction, walk the JSON tree and concatenate
text nodes (a small utility function, ~30 lines).
- Use
websearch_to_tsquery() for the query side to support natural search syntax.
- For the
matchField indicator, run two queries (title match + content match) or use a single SQL query with CASE WHEN to determine which field matched.
Reproduction
- Create 30+ documents in doc with different body content but similar titles.
- From a host app (e.g. RoleWeave memory panel), search for a keyword that only appears in one document's body.
- Observe: zero results, because only
title is searched.
Scope note
The "Memory & Collaboration" panel UI (file list, search input, "copy reference" button, "知识资料/全部文件" tabs) lives in the host application (e.g. bytefolk/roleweave), not in this repo. This issue covers only the doc-side API and database capabilities. The host app will need a corresponding issue to consume the new query params and render the enhanced results.
Phasing suggestion
| Phase |
Effort |
Impact |
| 1. Content full-text search |
Medium (migration + extraction utility + query change) |
High — solves the core "can't find by content" problem |
| 2. Time range filters |
Low (just add where conditions on existing DateTime fields) |
Medium — helps narrow down large result sets |
| 3. Metadata filters |
Medium (schema change for type/size) |
Low-Medium — useful as document diversity grows |
| 4. Structured query syntax |
N/A (host-app side) |
Convenience for power users |
Phase 1 alone would resolve the primary user complaint. Phase 2 is a small follow-up. Phases 3-4 can be deferred.
Summary
The document search APIs (
GET /api/docandGET /api/v1/documents) only match againstdoc.titleusing SQLLIKE/ILIKE. Host applications (e.g. RoleWeave's "Memory & Collaboration" panel) that embed doc as a read-only knowledge source cannot search document content, filter by time range, or sort by update/create date with any granularity beyond the defaultupdatedAt desc. When a workspace accumulates 30+ documents and the target keyword only appears in the body, the search returns nothing.This issue tracks upgrading the doc-side search infrastructure to support multi-dimensional indexed retrieval. The host-app UI layer is out of scope for this repo; this issue focuses on the API and database capabilities that host apps consume.
Current behavior (code evidence)
Internal API:
GET /api/docsrc/app/api/doc/route.ts(lines 231-234)whereOpt.title = { contains: keyword }(case-sensitiveLIKE)API v1:
GET /api/v1/documentssrc/lib/api-v1-documents.ts(lines 159-204){ title: { contains: query, mode: 'insensitive' } }(case-insensitiveILIKE)(updatedAt, id)— good foundationqueryis the only search dimension;starredandtrashare the only filtersPrisma schema:
Docmodelprisma/schema.prisma(lines 19-41)content: String(TipTap JSON) — available for indexing but not searchedcontentBinary: Bytes?(Yjs binary state)createdAt: DateTimeandupdatedAt: DateTime— available for range queries but not exposed as filtersFull-text search infrastructure
tsvector, no GIN index, no@@operator, no search migration.Read-only / shared access
src/lib/document-access.ts,prisma/schema.prisma(ShareRelation model)READ/WRITEaccess typesgetApiDocumentresolves access asowner/write/readdocuments:readscope already see shared documents — search just needs to respect the sameuserIdscopeExpected behavior
Phase 1 — Content full-text search (core)
contentSearchtsvector column (or equivalent) to theDoctable, populated from the plain-text extraction ofcontent(TipTap JSON → text).GET /api/v1/documentsquery paramqueryto search bothtitleand document content:query=keyword→ matches title OR content containing "keyword"matchFieldindicator in results (title|content|both) so the host UI can highlight where the match was foundGET /api/dockeywordparam with the same title+content search.containsfallback for environments where PostgreSQL full-text search is not configured (e.g. SQLite dev).Phase 2 — Time range filters
GET /api/v1/documents:after=2026-09-01— documents updated after this ISO datebefore=2026-09-17— documents updated before this ISO datesort=updated_asc|updated_desc|created_asc|created_desc(default:updated_desc)GET /api/doc.Phase 3 — Metadata filters
mimeTypeordocTypefield to theDocmodel (or a derived classification):application/tiptaptype=tiptap|markdown|json|...to filter by document type.minSize/maxSizequery params for content size filtering.Phase 4 — Structured query syntax (optional, host-UI side)
type:json after:2026-09-01 keywordand translate it into the API query params above. This is a host-app concern; doc just needs to provide the filter params.Suggested technical approach
tsvector+ GIN is the natural fit since the stack is already Prisma + PostgreSQL.contentSearchvia a trigger onINSERT/UPDATEof thecontentcolumn, or via application-level extraction in the create/update path.textnodes (a small utility function, ~30 lines).websearch_to_tsquery()for the query side to support natural search syntax.matchFieldindicator, run two queries (title match + content match) or use a single SQL query withCASE WHENto determine which field matched.Reproduction
titleis searched.Scope note
The "Memory & Collaboration" panel UI (file list, search input, "copy reference" button, "知识资料/全部文件" tabs) lives in the host application (e.g.
bytefolk/roleweave), not in this repo. This issue covers only the doc-side API and database capabilities. The host app will need a corresponding issue to consume the new query params and render the enhanced results.Phasing suggestion
whereconditions on existingDateTimefields)Phase 1 alone would resolve the primary user complaint. Phase 2 is a small follow-up. Phases 3-4 can be deferred.