# 索引流程

watcher 如何偵測資料庫 inbox 中新增、修改與移除的檔案，並把它們轉成 `file_data` 資料列。

## 輪詢與變更偵測

每個已載入的資料庫各自執行一個 watcher goroutine（`cmd/app/main.go` 的 `runWatcher`），計時器間隔 10 秒。每次觸發時 `filesystem.WalkFiles` 遞迴讀取 inbox，並與前一次快照逐項比對：

| 條件 | 判定 |
|---|---|
| 名稱不在快照中，或檔案／目錄類型不同 | 已變更 |
| 名稱、大小、修改時間皆相同 | 未變更，略過 |
| 名稱相同但大小或修改時間不同 | 已變更 |
| 名稱在快照中但磁碟上已不存在 | 已移除 |

子目錄一律會往下走訪，任意深度的檔案都會被索引。未變更的檔案不會重新讀取。

## 快照檔

每次走訪後，新的目錄樹（大小、修改時間、是否為目錄、子項目）會在背景 goroutine 寫入 `<db>/record.json`，並以 mutex 序列化。啟動時 watcher 會載入這份快照，因此 daemon 重啟後只會重新索引停機期間變動的檔案。沒有快照時，第一次觸發會索引所有檔案，並略過移除檢查。

## 寫入變更的檔案

變更的檔案依副檔名分派給對應 parser（見[支援的檔案格式](/zh/file-formats)），再由 `databaseHandler.Upsert` 在單一 transaction 內寫入：

1. 將該來源路徑所有有效資料列標記為 `dismiss = TRUE`。
2. 每個新 chunk 依 `(source, chunk)` 插入或更新，並設為 `dismiss = FALSE`。

因此編輯後已不存在的 chunk 會維持 dismissed，讀取端也不會看到同一檔案新舊 chunk 混雜。`source` 欄位存的是檔案在 inbox 內的絕對路徑。

| 結果 | `daemon.log` 紀錄 |
|---|---|
| 已儲存 | `saved kind=<type> path=<path> chunks=<n>` |
| Parser 錯誤 | `parser error=...`；保留既有資料列 |
| 寫入錯誤 | `store.Save error=...` |
| 略過的類型 | 不寫入，除 `changed` 外無其他紀錄 |

解析失敗的檔案仍會以當下的大小與時間記進快照，要等檔案再次變動才會重試。

## 移除的檔案

上次走訪後消失的每個名稱，都由 `databaseHandler.Dismiss` 把該路徑所有有效資料列標記為 `dismiss = TRUE`。移除整個目錄時，快照中記錄在其下的所有檔案都會被 dismiss。資料列不會被實體刪除，所有搜尋查詢都會排除 dismissed 的資料列。

## 相關頁面

- [支援的檔案格式](/zh/file-formats)
- [資料配置](/zh/data-layout)
