# 語意搜尋

說明 semantic branch 如何 embed query、以 source vector 縮小候選、排序 chunk，並從 SQLite hydrate 命中結果。

## Semantic query embedding

Semantic branch 先使用完整 query string 查詢 `openai.Cache`。Cache miss 時，會向 OpenAI 傳送單項 batch（`text-embedding-3-small`、512 維），並把 vector 存入記憶體。

| Process | Cache miss 之後 |
|---|---|
| Daemon | 透過 cache 的 `OnSet` callback 非同步持久化至 `global.db`，timeout 5 秒 |
| `kura mcp` | 未註冊 `OnSet` callback；vector 只存在該 session 的記憶體 |

兩種 process 啟動時都會從 `global.db` 預載 query cache，只保留 byte length 等於 `openai.Dim() * 4` 的 blob。Preload 會繞過 `OnSet`，因此不會重寫同一 entry。

## 兩階段 vector search

每個 database bucket 都儲存 chunk vector，以及每個 source 的一個 derived vector：該 source 同維度 chunk vector 加總後做 L2 正規化，chunk 變動時重建。

### 階段一：Source candidate

KuraDB 會計算 query 與每個同維度 source vector 的 cosine similarity。Candidate 數量為：

```text
min(max(number of sources / 20, 20), number of sources)
```

大型 source set 約保留 5%；若數量足夠，至少保留 20 個。

### 階段二：Chunk ranking

KuraDB 收集 candidate source 的 chunk ID，再為每個 chunk 計算同維度 cosine similarity。Worker 數量為 `min(CPU count - 1, chunk count / 200, chunk count)`，至少一個，因此資料量足夠時每個 worker 至少處理 200 chunks。Hit 依 score 降冪排序，再截斷為 `topK`（即 request limit）。

若沒有 source vector，search 會 fallback 為直接排序所有 chunk vector。

## Score 過濾與 hydration

搜尋核心會移除 score 低於 `0.3` 的 semantic hit，接著以 `dismiss = FALSE` 從 SQLite 取得其餘 ID、恢復 vector ranking order，並丟棄已無法解析為 active row 的 ID。

即使記憶體 state 同時發生變更，此 final hydration 仍維持 SQLite 的權威性。留下的 row 如何分組與回傳，見[搜尋結果](/zh/search-and-retrieval-results)。
