總覽與痛點
先問「為什麼 keyword 不夠」,再談向量資料庫——語意相近,但用字不同時怎麼找?
假設知識庫裡有一段話:「如何在分散式系統裡保證資料不遺失?」使用者卻搜尋「多機備份會不會掉資料」。若只做關鍵字比對(倒排索引 + BM25:依詞頻/稀有度打分的關鍵字排序),兩邊幾乎沒有共同詞,結果可能是空的——但對人來說,這兩句意思很接近。
向量搜尋的做法是:先把文字(或圖片)轉成一串數字——Embedding(嵌入向量)——讓「意思接近」的內容在數字空間裡距離也接近;查詢時同樣轉成向量,找出距離最近的 Top-K 筆。承擔這件事、並在大規模下仍能毫秒級回應的系統,就是常說的向量資料庫(Vector Database)。
• OLTP(PostgreSQL):WHERE id = ?、交易——不擅長「跟這句話意思像不像」。
• 全文搜尋(Elasticsearch):關鍵字 / BM25——擅長用字重疊,不擅長純語意近似。
• 向量搜尋:高維距離 Top-K——擅長語意,專有名詞、型號、精確詞常仍要靠 keyword 互補。
實務上兩者常並用(Hybrid Search),不是互相取代。
若查詢本質是精確 ID、固定欄位過濾、或「必須出現某個型號字串」——先用 SQL / 倒排索引。向量搜尋適合「意思像不像」;語料極小(幾百筆)時,甚至直接算全量距離就夠,不必急著上專用向量庫。
• 本篇聚焦:Embedding → 索引(ANN)→ 檢索品質(recall)→ 何時用 pgvector/專用庫。
• 名詞對齊:業界常把「存向量 + 做近鄰搜尋」的元件叫 Vector Store(應用/框架用語)或 Vector Database(獨立服務)。兩者都在做同一件事;本篇用「向量資料庫」統稱。
• 輕觸即可:量化壓縮、寫入後索引何時可查——在 §05 用最短路徑交代,不當成完整儲存引擎專題。
向量長什麼樣子
先建立畫面:向量就是座標;相近的意思會聚在一起。先别管 768 維,用 2D 想即可。
把每段文字想成平面上的一個點。Embedding 模型的工作,是讓「語意相近」的點靠得近、「不相干」的點離得遠。查詢時:把問題也變成一個點(下圖的 ★),再找離它最近的幾個點——這就是 Nearest Neighbor / Top-K。
| 內容(示意) | 向量(簡化成 2 維) | 與 Query 的距離 |
|---|---|---|
| Query:「多機備份會不會掉資料」 | (0.62, 0.55) ★ | — |
| 「分散式系統如何保證資料不遺失」 | (0.60, 0.58) | 很近 → 應命中 |
| 「資料庫主從複寫與 failover」 | (0.58, 0.50) | 近 → 常進 Top-K |
| 「今天午餐吃什麼」 | (0.12, 0.90) | 很遠 → 不該出現 |
真實系統是幾百~上千維,人眼看不見,但「距離近 = 較相似」的直覺相同。§06 Demo 會用 2D 畫布讓你親手點 query。
常見距離/相似度
- L2(歐氏距離):就是平面上兩點的直線距離——上表用的直覺。圖像 embedding 常見。
- Cosine Similarity:比「方向」像不像,不太在乎向量長短;文字最常用。實務常先做 L2 normalize(把長度收成 1),再比角度。
- Inner Product(內積):向量已 normalize 時與 Cosine 等價;若沒 normalize,長度會干擾分數,別跟 Cosine 混用。
| 場景 | 常見選法 | 記住一句 |
|---|---|---|
| 文字/RAG chunk | 先 L2 normalize,再用 Cosine 或 IP | 比方向,不比長短 |
| 圖像 embedding | 常直接 L2 | 空間距離本身有意義 |
| 索引與查詢混用不同度量 | — | 分數不可比,等於換了一個空間 |
§06 Demo 用 L2 在 2D 示意「近/遠」;文字系統多半是 normalize 後的 Cosine/IP,但「愈近愈像」的決策邏輯相同。
資料量小時可以——這種精確全掃下文統一叫 Flat(也常寫成 brute-force kNN),命中率是滿分。但當向量到數百萬~數億,每次查詢都算 N 次距離會變成秒級。於是需要 ANN(Approximate Nearest Neighbor,近似最近鄰):用索引少看一些點,用略降的命中率換毫秒級延遲。
假設「真正最近的 5 個點」是標準答案,ANN 也回傳 5 個。若其中 4 個落在標準答案裡,則 recall@5 = 4/5 = 80%。後面說 recall↑,就是指「該中的有沒有被撈回來」——跟 API 延遲是兩條不同的 SLA。
Embedding 管線:從文件到可搜尋
第 2 節的「點」在實務上通常代表一段文字,不是整本書——所以要先切段再 embed。
真實文件往往太長:若整本手冊只變成一個向量,查詢時只能說「這本書有點相關」,無法指出哪一段。因此要先 Chunk(切段),讓空間裡的每個點對應可引用的片段。
這是兩條各自獨立觸發的路徑:上面一條每份文件只跑一次,下面一條每次提問跑一次,兩者在索引處交會——而且必須用同一個 embedding 模型、同一維度,否則距離根本不可比。這也就是下方「換模型 = 重建世界」的具體意思:兩條路徑都得整條重跑。
- Chunking:長文切成片段。模型內部用 token(子詞單位,可粗想成「比字元大、比句子小的計長單位」)限制長度;實務常切成約數百 token——大約半頁~一頁說明文字的量級。一頁 PDF 常見會變成數個 chunk。切太細語意破碎;切太粗一次撈回太多雜訊。邊界常留 overlap(重疊):實務常見約 10%~20% 的長度,讓跨段關鍵句兩邊都留得到。
- Embedding:呼叫模型把 chunk 轉成 d 維向量(常見 768~1536)。建索引與查詢必須同一模型、同一維度。
- Metadata:一併存
doc_id、tenant_id、source等,供過濾(權限、租戶、時間)。注意:若先 ANN 取 Top-10 再過濾,可能只剩 1~2 筆——看起來像「搜尋很空」。能 pre-filter(先縮小候選再搜)就先縮小;細節見 §07。 - Query path:使用者問題同樣 embed → ANN 取 Top-K →(可選)rerank(用更重的模型把候選再排一次)→ 把原文 chunk 當上下文交給 LLM。
RAG(Retrieval-Augmented Generation)=先檢索相關片段,再把片段塞進 LLM 提示詞裡生成答案。本篇聚焦前面的檢索(向量/ANN);生成品質是下一層問題。所以「RAG 答錯」時,要先問:正解 chunk 有沒有被撈到?
Chunk 切壞的後果(反例)
| 切法 | 發生什麼 | 使用者感受 |
|---|---|---|
| 切太細(一句一切) | 「主從複寫」與「會不會丟資料」拆成兩段向量 | 問完整問題時,兩段都不夠像 → 漏檢 |
| 切太粗(整章一段) | 一段向量混雜多主題 | 撈得到文件,但上下文又臭又長 → LLM 答得糊 |
| 合理 + 少量 overlap | 邊界句子兩邊都留一點(約 10%~20%) | 較不易在段落接縫處漏掉關鍵句 |
不同模型的向量不在同一空間(768 維 A 與 1024 維 B 不能比距離)。Production 換模型 = 全量 re-embed + 重建索引,需要雙寫或 alias 切換期。別以為「只改 API 名稱」。
很多「胡謅」其實是檢索沒把正確 chunk 撈上來(recall@K 低)。先查 chunk 策略、ANN 參數、要不要加 keyword hybrid,往往比換更大生成模型有效。
ANN:為什麼要近似,HNSW 與 IVF 差在哪
精確全掃太慢時,用「少看一些點」換速度;漏看就可能漏檢——這就是 recall vs latency。
精確 kNN:對每個向量算一次距離,複雜度約 O(N·d)。ANN 透過資料結構剪枝,讓查詢只檢查一部分候選,目標是:recall@K 仍夠高,P99 延遲可接受。
兩種常見思路(先建立機制,再記產品名)
nprobe),只在這些桶裡算距離。nprobe 愈大,看得愈多 → recall@K↑、延遲↑。
ef 就是「候選名單最多留多寬」。ef 愈大,走得愈勤 → recall@K↑、延遲↑。§06 Demo 的 SEARCH TRACE 會顯示「擴張了幾步、訪問了幾點」。
| 索引 | 你在少看什麼 | 調什麼 | 直覺取捨 |
|---|---|---|---|
| Flat | 什麼都不少看 | — | recall@K = 100%;大資料不可行 |
| IVF | 沒被 probe 到的 cluster | nprobe |
實作直覺、可搭配量化省記憶體 |
| HNSW | 沒走到的圖節點 | ef(查)、M(建圖) |
高 recall、低延遲常見首選;較吃記憶體 |
把參數壓到「1ms」很漂亮,但 recall@10 = 70% 表示每 10 個該中的大約漏 3 個——檢索層先壞了。SLA 應同時寫:recall@K ≥ X% 且 P99 ≤ Y ms。§06 Demo 會讓你同時看到命中、漏檢與「實際比較了幾次距離」。
pgvector 與專用向量庫:何時升級
多數後端團隊的務實路徑:先在 PostgreSQL 驗證,量級與 SLA 撞牆再上分散式向量庫。
pgvector:你已經會的資料庫加一欄向量
pgvector 是 PostgreSQL extension:表上加 vector(d),可建 HNSW / IVFFlat,並用 SQL 做 metadata filter + 排序。優點是零新基礎設施、ACID、JOIN;限制是單機記憶體/寫入天花板,到億級向量通常不夠。
CREATE EXTENSION vector;
CREATE TABLE doc_chunks (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding vector(1536),
tenant_id INT
);
CREATE INDEX ON doc_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- ef_construction:建圖時的搜尋寬度(愈大圖通常愈好,建置愈慢)
-- 查詢時另有 ef:控制「這次走路走多勤」,影響 recall / 延遲
SELECT id, content
FROM doc_chunks
WHERE tenant_id = 42
ORDER BY embedding <=> $1 -- <=> = cosine 距離,愈小愈像
LIMIT 10;
專用向量庫:為規模而生
Milvus、FAISS(常作函式庫嵌入服務,而非獨立 DB)這類系統把「向量集合、分片、多種 ANN、水平擴展」當一等公民。適合億級向量、高 QPS,或需要把索引/向量壓進更少記憶體時。代價是多一套運維,metadata 也不如 SQL 靈活。內部還有 collection/segment 等名詞——決策階段可先略過,當成「可水平切的索引分片」即可。
原始 float32 × 1536 維很吃記憶體。量化用較少位元近似每個向量(常見:SQ 標量量化、 PQ 乘積量化——把高維切段後各段用 code book 編碼)。
效果:記憶體↓、可快取更多向量 → QPS↑;代價是距離變成近似值,recall 可能略降,需要用更高的 nprobe/ef 或 rerank 補回來。
• Flat:寫入即可比距離,無額外索引結構。
• HNSW:新點可增量插入圖,但大量寫入後圖品質會漂,偶發需要 rebuild 才能維持 recall。
• IVF:centroid 是離線分群結果;資料分布大變時,舊桶邊界失準 → 常需定期重訓 centroid 再重建 list。
Production 要問清楚:upsert 是「立刻可搜」還是「進 buffer,背景合併後才進 ANN」。
| 決策線索 | 偏 pgvector | 偏專用 Vector DB |
|---|---|---|
| 規模 | 大約 < 數百萬 chunk | 億級、要水平擴 |
| 查詢型態 | 強依賴 SQL JOIN / 交易 | 檢索為主、metadata 過濾即可 |
| 團隊成本 | 已有 PG,想快驗證 | 願意養叢集或買託管 |
內部 10 萬 chunk 的知識庫,運維成本常大於收益。先 pgvector 或託管 Serverless 驗證產品;出現 CPU/記憶體飽和、建索引超時、P99 破 SLA、需要跨機 shard 再遷。
雙寫(或 CDC)→ 同一組 query 比 recall 與 P99 → 讀流量灰度。常見切法:PG 繼續當 metadata/權限的 source of truth,向量檢索切到專用庫。
互動演示:空間結果與搜尋過程
左側看命中/漏檢;右側看這次查詢「探了哪些桶/走了哪些候選」以及實際距離比較次數。
畫布上有一組 2D 點(模擬向量)。點一下可移動 Query ★。左側列表的 GROUND TRUTH=用 Flat 全掃算出的「正確 Top-5」,用來對照 ANN 有沒有漏。預設已選 Flat,請依序試:
- Flat——全掃,recall@5 應為 100%,比較次數 = 全部點。
- IVF + 小 nprobe——畫布出現 cluster 圓;實線粗圓 = 有 probe。只探少數桶時,看漏檢(橘)與比較次數下降。
- IVF + 大 nprobe——recall 回升、比較次數上升。
- HNSW——切換後預設
ef=16(教學起點,通常已接近滿分);再把 ef 往下調到 2~4,觀察漏檢與比較次數下降。淡線是鄰近邊,亮一點的是走過的邊;右側 TRACE 顯示擴張步數與訪問點數。
這是 2D 教學模型:IVF 真的只掃被 probe 的 cluster;HNSW 真的只沿預建鄰近邊(含少量跨區跳接)擴張。真實高維 HNSW 有多層、曲線更陡,仍需離線 benchmark 定參。
面試時怎麼講:RAG 檢索與折衷
把前面串成可答的架構故事:檢索差先查 recall,再決定調 ANN、chunk 或 hybrid。
- Ingestion:文件 → chunk → embed → upsert(+ metadata)。寫入後索引是否立刻可搜,見 §05。
- Retrieval:query embed → ANN Top-K →(可選)rerank → 當 LLM 上下文(RAG 的檢索段)。
- Hybrid:BM25 與向量各取一截,再把兩路排名合併(常見手法叫 RRF:兩邊名次互相加權,不必調分數量綱)——專有名詞靠 keyword,語意靠向量。
- Filter:假設 ANN 取 Top-10,再按
tenant_id過濾只剩 2 筆——使用者會覺得「搜尋很空」。能 pre-filter(先縮小再 ANN)就先縮小;只能 post-filter 時,要把 K 加大或換支援過濾的索引路徑。
RRF 合併:一個最小例子
假設向量路與 BM25 路各回傳排名(名次愈小愈好)。RRF 常用分數:1 / (60 + rank),兩路加總後再排序——重點是用名次、不用原始分數,所以不必把 Cosine 與 BM25 硬齊到同一量綱。
| chunk | 向量名次 | BM25 名次 | RRF(示意) |
|---|---|---|---|
| A(語意很像,但沒出現型號字) | 1 | 5 | 1/61 + 1/65 ≈ 0.0318 |
| B(有精確型號,語意普通) | 4 | 1 | 1/64 + 1/61 ≈ 0.0320 → 常會浮上來 |
| C(兩路都普通) | 3 | 4 | 1/63 + 1/64 ≈ 0.0315 |
上表只示範「名次相加」的直覺;常數 60 可調,但面試講清楚「為何不用原始分數相加」比背數字重要。
• pre-filter:先用 metadata(租戶、ACL、時間)縮小集合,再跑 ANN——Top-K 幾乎都是「合法且相關」。
• post-filter:先 ANN 再丟棄不合格——若過濾很嚴,要把 K 加大(例如先取 50 再濾),否則結果會空。
• 產品若強制多租戶隔離,優先選支援過濾感知索引(filtered HNSW/分 collection)的路徑,而不是只靠應用層事後丟棄。
1. 用標注 query–chunk 量 recall@K。
2. 低 recall → 調 ef/nprobe、檢查 chunk/overlap、試 hybrid。
3. recall 高仍答錯 → reranker 或 prompt/模型問題。
4. 延遲高 → 減 K、量化、或擴 query 節點——但先確認沒為了速度犧牲太多 recall。