Relay · Search
搜尋 channel
Relay 在單一 channel 內提供 ranked full-text search。Search 是 derived read model。Message store 仍是 source of truth,post-commit worker 會 asynchronous update search index。
目前的 public SDK 尚未提供 client.search(query)。請從 host client 呼叫 HTTP endpoint,或包在 host-owned API layer 後方。
Request
Caller 必須是目前的 channel member:
GET /api/channels/{channel_id}/messages/search?q=refund&limit=20
Authorization: Bearer <jwt> | Parameter | 條件 |
|---|---|
q | Required,trim 後為 1 到 200 個 Unicode character |
limit | Optional,default 20,maximum 100 |
cursor | Optional,使用前一頁回傳的 opaque cursor |
limit 應使用 1 到 100 的正整數。目前 endpoint 的 lower-bound validation 不完整,所以 0 或負數不屬於 public contract。
Default Postgres search path 使用 web-search syntax,支援 term、quoted phrase、OR,以及以 leading minus 排除內容:
"payment failed" OR refund -sandbox 目前沒有 sender、date range、mention、attachment type 或 minimum rank filter。
Response
{
"data": {
"hits": [
{
"message": {
"id": "message-uuid",
"channel_id": "channel-uuid",
"sender_id": "user-uuid",
"body": "The refund is ready",
"seq": 42,
"attachments": []
},
"rank": 0.18,
"snippet": "The <mark>refund</mark> is ready"
}
],
"next_cursor": null,
"degraded": false
}
} 每個 hit 包含一般 Relay message payload,以及 adapter 產生的 rank 與 snippet。沒有下一頁時,next_cursor 是 null。
安全顯示 snippet
snippet 是 untrusted user content,其中混有 adapter 插入的 <mark> tag。它不是 safe HTML fragment。不要直接指定給 innerHTML 或 Svelte {@html}。
目前最安全的方式是把 snippet 當成 text render。需要 highlight 時,請使用只允許 mark 的 strict allowlist sanitizer,或由 client 從 message body 重新產生 highlight。
Structured highlight-range response 與 server-escaped snippet contract 尚未實作。
Indexed content
目前的 Postgres index 會 match:
- Message body
- Bound attachment filename
Attachment file byte 不會進入 index。Sender ID 與 mention token 存在於 derived document model,但沒有加入目前的 Postgres search vector。
Filename-only match 可能回傳沒有顯示該 filename 的 snippet,因為 snippet 只從 message body 產生。請使用 returned message 的 attachments metadata 顯示 filename。
Ordering 與 cursor
Primary search result 使用以下順序:
rank DESC, inserted_at DESC, message_id DESC Cursor 必須保持 opaque,且只能在相同 tenant、channel、query 與 search mode 下重用。只有在相關 index row 沒有變化時,pagination 才保持穩定。
目前的 cursor 有兩項已知限制:
- 無法 decode 的 cursor 會被視為沒有 cursor,並重複第一頁,不會回傳
422。 - Cursor 沒有簽章,也沒有綁定 original query 與 channel。
Query 改變時,client 應丟棄 stored cursor。Pagination 期間 index 發生變化時,應以 message.id deduplicate hit。
Index consistency
Message create、edit、retract 與 delete operation 會寫入 durable post-commit handoff。Search indexing 發生在 conversation transaction commit 之後。
因此目前的行為是:
- New 或 edited message 可能不會立即出現。
- Retracted message 的 indexed body 為 empty,但 attachment filename 仍可能 match。
- Deleted message 會 asynchronous remove from index。Relay 在回傳 hit 前也會 filter deleted source message,所以 stale index row 不會成為 visible message。
- Pin 與 unpin 不會改變 search document。
Relay 目前沒有公開 maximum index-lag objective。需要 read-after-write consistency 的 workflow,應透過 conversation API 讀取 message,不要使用 search。
中文搜尋模式
Relay 在 production 會要求使用 zhparser 執行中文斷詞。Postgres extension 不可用時,startup 會 fallback 到 simple_bigram,不會停用 search。
CJK query 使用 fallback path 時,response 會標記:
{
"degraded": true
} Degraded path 使用 substring bigram,並把 query 限制在前 20 個 unique bigram。Web-search operator 在這條 fallback path 不保留一般語意。
Degraded pagination 尚未完成:fallback 不會套用 incoming cursor,可能重複第一頁。degraded 為 true 時,請把 response 視為單頁 fallback,並忽略 next_cursor。
Error 與 limit
| Status | 意義 |
|---|---|
401 | JWT 缺少或無效 |
403 | Caller 不是 channel member |
422 | q 缺少、為 empty,或超過 200 個 character |
429 | User 或 tenant search budget 已耗盡 |
目前 default 是每個 user 每分鐘 60 次,每個 tenant 每分鐘 600 次。429 response 的 Retry-After 使用秒數。
Operator backfill
Operator 可以 enqueue tenant search backfill,並查看 state:
POST /api/admin/tenants/{tenant_id}/search/reindex
GET /api/admin/tenants/{tenant_id}/search/reindex
X-Admin-Token: <admin-token> POST 回傳 202 Accepted。State endpoint 會回報 never_run、running、complete 或 failed,以及 indexed-row count 與可用的 failure detail。
雖然 route 名稱是 reindex,目前的 operation 是 resumable backfill,不是 clean rebuild。Completed cursor 不會在後續 run 前 reset,stale index row 不會先清除,而且 backfill document 不包含 attachment filename。Operator 不能以這個 endpoint 證明所有 derived search row 都已精確重建。
Product boundary
Channel membership、tenant-scoped index、ranked body search、attachment filename matching、cursor pagination、asynchronous update、中文 fallback、rate limit 與 operator backfill 已實作。
Safe structured highlight、strict cursor validation、cursor binding、reliable degraded pagination、positive limit validation、index-lag objective、exact rebuild semantics、attachment-aware backfill、advanced filter 與 SDK search helper 仍屬於 product work。Meilisearch adapter 目前只是 skeleton,不能作為 production backend 啟用。
下一步:接收已簽章的 webhook event。