Skip to content

Ask AI 的文章怎麼進知識庫:Chunk、D1 FTS5 與 Vectorize

2026年8月30日1 分鐘
TL;DRAsk AI 的 production 索引分兩階段:先用 source hash 增量更新 D1、post_chunks 與 FTS5,再以 embedding checkpoint 和 delete queue 讓 Vectorize 非同步追上;兩邊不是同一個 transaction。

查詞與收藏

正在載入查詞工具…

🌏 English version

搭配閱讀(選讀):零基礎可以直接讀本文。想先補概念,可搭配〈Chunking 策略:切塊方式決定 RAG 能不能找到答案〉與〈Vector Database 選型:Pinecone、Weaviate、Qdrant、Vectorize 怎麼選〉。

一篇 Markdown 文章存進 repo,不會立刻變成 Ask AI 的可用證據。它還要通過發布資格、切成 chunk、寫入 D1、加入 FTS5,最後產生 embedding 並 upsert 到 Vectorize。這幾步若只完成一半,BM25 與向量搜尋看到的資料就可能不同步。

Ask AI 的 production indexing 刻意分成兩個階段:D1、post_chunks 與 FTS5 在一個 D1 batch 內對齊;Vectorize 再透過 checkpoint 與 delete queue 非同步追上。 這不是一個涵蓋兩種儲存的 transaction。理解這條界線,才知道「文章已部署」「全文找得到」和「向量已更新」為什麼是三件事。

第一步:只收進可被搜尋的文章

scripts/sync-to-d1.ts 遞迴掃描 src/content/posts/**/*.md,解析 frontmatter 與正文。以下文章會被排除:

  • draft: true。
  • search: false。
  • 日期尚未到,而且這次沒有明確使用 --include-future。

符合條件的文章會整理出 slug、title、category、language、description、tldr、tags 與正文。程式再把這些欄位連同 sync schema version 算成 source_hash。Production sync 先向 /api/index/posts/sync 取得遠端 slug/hash manifest,只有 hash 改變的文章才送 upsert;遠端存在、目前 manifest 不再出現的 slug 則送 delete。

因此一般內容更新是增量同步,不是每次 push 都重建所有文章。--full 是明確的 maintenance path,不能把它當成日常 deploy 的預設行為。

Chunk ID 可重算,但跟文章內的位置有關

chunkMarkdown 先依 H1–H3 標題切 section。單一 section 若超過 1,500 個字元,再依段落累積切開。每個 chunk 的 ID 是:

sha256(slug + "::" + chunk_index).slice(0, 16)

固定公式讓同一個 slug 與位置能重算出同一個 ID,也讓 D1 row、FTS row 與 Vectorize vector 可以用 chunk_id 對接。但它不是內容雜湊:若文章前面新增一段,後續 chunk index 改變,後面的 ID 也可能一起改變。

同步程式還會替每個 chunk 建立 contextual content,加入文章標題、分類與日期,再以 embedding version 和 contextual content 計算 desired_embedding_hash。正文沒變但 embedding version 改變時,checkpoint 仍能把該 chunk 標成需要重新 embedding。

D1、post_chunks 與 FTS5 在同一批寫入

Production endpoint /api/index/posts/sync 要求 shared secret,限制 request body,並對 operation 數量與 D1 statement 數設上限。解析完成後,post-sync.ts 把一批 operation 交給 DB.batch。

一篇文章 upsert 的順序是:

  1. 把舊 post_chunks.id 放進 vector_delete_queue。
  2. 刪除舊的 FTS rows 與 post_chunks。
  3. upsert posts 主表與新的 source_hash。
  4. 插入新的 chunks,寫入 desired_embedding_hash,並把 embedded_hash 設為空。
  5. 若新 chunk ID 仍存活,從 delete queue 移除它。
  6. 插入對應的 chunks_fts row。

刪除 stale post 時也先 queue 舊 vector ID,再移除 FTS、chunks 與 post。這個順序避免 D1 已刪掉來源後,Vectorize 裡的舊 ID 完全失去清理線索。

FTS5 目前由 0025_search_cjk_trigram.sql 建成 trigram index。它改善三個字以上的 CJK substring matching;兩個字的 query 仍需要應用層 LIKE fallback,不能把 trigram 寫成所有中文短詞都能直接命中。

Vectorize 用 checkpoint 非同步追上

D1 同步完成後,第二階段才呼叫 /api/embed/sync。這個 endpoint 要求管理員 session 或 shared secret,並限制 sources、batch size 與 full 參數。

embedPosts 的順序很保守:

  1. 先 drain vector_delete_queue,每批刪掉舊 Vectorize IDs。
  2. Queue 尚未清空時,不做新 vector upsert,回傳 hasMore: true。
  3. 查出 embedded_hash 為空或不同於 desired_embedding_hash 的 chunks。
  4. 替正文加上 title、category、date context。
  5. 用 Workers AI 產生 embedding,upsert 到 VECTORIZE_INDEX。
  6. Vectorize 成功後,才把 D1 的 embedded_hash 更新成 desired value。

如果 embedding 或 upsert 失敗,錯誤會被記在結果裡,checkpoint 保持 pending,下一批可以重試。這能提供可恢復的增量流程,但不代表 D1 與 Vectorize 永遠零延遲同步;兩個階段之間的短暫 lag 是這個設計允許的狀態。

Deploy workflow 只在相關內容改動後接上 indexing

deploy.yml 會先完成 build、migration 與 Worker deploy。只有 content-index detector 判定需要同步,而且 deploy job 成功後,才呼叫 reusable content-index.yml。

Reusable workflow 依序執行 production D1 sync、pending embedding sync,最後跑 search freshness check。Embedding driver 會反覆呼叫 endpoint,直到 hasMore 為 false或碰到設定的 max-batches 上限。

Workflow 的存在能證明預定執行順序,不能證明最近一次 workflow 已成功,也不能證明目前 production 沒有 pending checkpoint。要回答現況,仍要看該次 run 與遠端儲存。

重跑本機契約測試

pnpm exec vitest run \
  src/lib/indexing/post-sync.test.ts \
  src/lib/indexing/pipeline.test.ts \
  src/pages/api/index/posts/sync.test.ts \
  src/pages/api/embed/sync.test.ts

這組測試固定 operation 上限、D1 batch 順序、delete queue、checkpoint ack 與 endpoint authorization 等不變量。它不會查 production 儲存內容。

若你有 Cloudflare 權限,可以用唯讀 SQL 查 pending checkpoint:

npx wrangler d1 execute quidproquo-db --remote --command="
SELECT
  COUNT(*) AS chunks,
  SUM(embedded_hash = desired_embedding_hash) AS embedded,
  SUM(embedded_hash IS NULL OR embedded_hash != desired_embedding_hash) AS pending
FROM post_chunks;"

這個查詢能看到 D1 checkpoint,仍不能單獨證明 Vectorize 中每個 ID 與值都正確。Vector count、D1 checkpoint 與一次真實 retrieval observation 要分開保存。

證據邊界

Repo 與單元測試能證明索引流程的程式契約:增量 hash、chunk ID、D1/FTS batch、delete queue、embedding checkpoint 與 workflow 順序。它們不能證明 production 索引目前完整、freshness check 最近通過,或某篇文章一定能被某個 query 召回。

另外,pnpm sync 的 local path 只同步本機 D1 與 FTS,不能拿來證明 production Vectorize checkpoint 流程。下一篇會從索引的另一端出發,追 Planner 如何把使用者問題送進 metadata、BM25 與 vector lanes。

已知限制與改進方向

上面描述的 pipeline 能完成增量同步、FTS 與向量一致性追蹤,但 chunking 階段有幾個已知弱點,值得記在這裡。

表格切塊會丟失表頭

chunkMarkdown 依 H1–H3 和段落邊界切分。當一張 Markdown 表格跨越 1,500 字元邊界,只有第一個 chunk 保留表頭(|...| + |---|)。後續 chunk 變成一堆無上下文的 | cell | cell | 行,embedding 品質明顯下降——向量無法反映「這是哪張表、哪個欄位」。

最小改動的修補是 Header Propagation:splitter 切完後掃描每個 chunk,偵測到 Markdown 表格行但缺少表頭時,把原始表頭 prepend 回去。開源工具 Chonkie TableChunker 已實作此模式:「splits markdown tables by row, always preserving the header」。arXiv:2605.00318 的 STC 框架更進一步,以 row 為最小單位切分表格,在 MAUD 法律文件上把 Recall@1 從 0.347 提升到 0.539,chunk 數量反而減少 40%。

Header Propagation 每個 chunk 多約 50 tokens,受影響的文件需要重新 embed。

Metadata Enrichment 可以再做更多

目前 pipeline 在 embedding 前 prepend 文章標題、分類與日期。這已經是輕量版的 metadata enrichment。arXiv:2601.11863(Utilizing Metadata for Better RAG)發現公司名稱加年份提供最強的區分訊號;章節標題路徑主要幫助 chunk 級別的定位。Microsoft Azure RAG Enrichment Phase 建議的完整欄位是 Title、Summary、Keywords、Questions——後兩者需要 LLM 生成,但前兩者不需要額外成本。

對 Ask AI 來說,下一步是把章節標題路徑(H1 → H2 → H3)也加進 contextual content。這不需要 LLM,改動範圍小,可以跟 Header Propagation 疊加。

Contextual Chunking 是主要升級路徑

Anthropic Contextual Retrieval 在 ingestion 時用 LLM 為每個 chunk 生成一段 50–100 tokens 的上下文描述,prepend 後再 embed。依 Anthropic 官方數據,搭配 reranking 後 top-20 retrieval failure rate 降低 67%。成本約 $1.02/百萬 document tokens(使用 prompt caching)。

這不只解決表格問題——所有 chunk 的 embedding 品質都會提升。目前 Ask AI 已使用 Claude,prompt caching 可控成本。但需要一次性重新處理所有 chunks。

Late Chunking 用另一條路省掉 LLM

Late Chunking(Günther et al., EMNLP 2024 / SIGIR 2025)反轉傳統的「先切再 embed」順序:把整份文件丟進 long-context embedding model(8K+ tokens),讓 attention 看過全文後才切分。每個 chunk 的 embedding 天然帶有全文上下文,不需要額外 LLM 生成描述。

限制是需要支援 8K+ context 的 embedding model(如 jina-embeddings-v3),而且大文件可能超出 context window。目前 Ask AI 使用 Cloudflare Workers AI 的 embedding model,context 長度是一個需要確認的前提。

更多改進方向:Hierarchical Chunking + Auto-Merge 用多粒度索引解決「小 chunk 精準但上下文不足」的矛盾;Table Serialization 探討表格序列化格式對 embedding 品質的影響。

更新紀錄

  • 2026-09-03:補充「已知限制與改進方向」段落與四篇參考資料

參考資料