SPEC-STV-09-Search
SPEC-STV-09 · Spec header. Spec ID: SPEC-STV-09 · Title: Search · Version: 1.0.0 · Status: Planned · Authority: Specification · Priority: P1 · Owner role: Backend architect · Reviewers: Frontend lead, DevOps architect · Last reviewed: 2026-05-11 · Sync targets: app/Services/Search/**, Meilisearch config · Depends on: SPEC-STV-HUB, SPEC-STV-02, SPEC-STV-05 · Consumed by: SPEC-STV-03, SPEC-STV-10 · Conflict rule: Hub wins. · Change policy: Backend architect + Frontend lead; Registry bump on driver change.
1 · Driver
- v1 default: Meilisearch via Laravel Scout. Self-hosted or managed.
- Fallback: MySQL
FULLTEXTindex for environments where Meilisearch is unavailable; controlled bysearch.driver.
2 · Indexes
| Index | Indexed entity | Searchable attrs | Filterable attrs | Ranking |
|---|---|---|---|---|
stv_pages | pages | title, plain_text (denormalized concat of block text) | workspace_id, status, visibility, parent_page_id, archived_at, author_id | typo → words → proximity → attribute (title boosted) → exactness |
stv_blocks | page_blocks | plain_text | workspace_id, page_id, type | same; capped at 200 KiB per doc |
stv_comments | comments | body | workspace_id, page_id, author_id, resolved_at | recency boost |
stv_files | files | original_name, extracted_text (when available) | workspace_id, mime, uploader_id | name boosted |
stv_databases | documentation_databases | name | workspace_id, page_id | — |
stv_db_rows | database_rows | denormalized concat of all value_text cells | workspace_id, database_id, archived_at | recency |
stv_templates | templates | name, description | workspace_id, category | — |
3 · Indexing pipeline
- Eloquent
Searchabletrait wires Scout.
toSearchableArray()populates the denormalized text fields (e.g. forPage: aplain_textfield built by concatenating block text viaBlockService::renderText($block)).
- Indexing is queued (Scout's queued mode); listeners on
page.updated,block.updated,comment.created,database_row.cells.updatedenqueue updates.
- Bulk reindex:
php artisan scout:importper model.
4 · Permission filter at query time
Every search call resolves the caller's accessible page set:
- Build a Redis set
search:user:{uid}:pages(TTL 60 s) fromPermissionsResolver.
- Send query to Meilisearch with
filter = workspace_id = X AND page_id IN […](Meilisearch supportsINwith up to thousands; usepages_visible_to_userprecomputed for fast filter).
- For workspaces with > 50k pages, fall back to post-filter on returned IDs (with adaptive page-size).
No result the caller cannot read is ever surfaced.
5 · Cmd / Ctrl + K palette
The global command palette opens with ⌘/Ctrl + K. Three sections:
- Quick actions — new page, new database, invite member, jump to settings.
- Recent — last 10 visited pages.
- Results — federated across the indexes, grouped (Pages, Blocks, Files, Databases, Comments, Templates).
Keyboard nav: arrow keys, Tab to switch group, Enter to open, Cmd/Ctrl + Enter to open in new tab. Mobile: a full-screen sheet with the same structure.
6 · Filters
type:page|block|file|database|comment|template
in:(page UUID) limits to a subtree
author:@meor@user
is:archived|favorite
after:YYYY-MM-DD,before:YYYY-MM-DD
Parsed by SearchQueryParser; unparsed terms become free text.
7 · API
See SPEC-STV-03 §8. Response shape per group: { group, hits: [{ uuid_or_id, title_or_excerpt, snippet, page_uuid? }], total }.
8 · Latency targets
p95 < 250 ms when index sizes < 1M documents per workspace. Federated query timeout 800 ms; partial results surfaced with a partial: true flag.
9 · Search-inside-page
A separate Cmd/Ctrl + F-style overlay that searches only the open page using client-side string match against the loaded blocks. No API call.