1.背景
最近在做一個agent項目,涉及到了pgvector向量數據庫的語義檢索檢索。
碰到了這樣的一個奇怪現象:數據庫裏明明有數據,但是什麼也查不出來,我也沒有用where對檢索進行限制,只是做了order by;當去除order by後才能查出東西來
我執行的語句類似如下所示,這個語句很正常,就是根據傳入的向量,到向量數據庫中查詢相似度最高的topK條記錄並返回,可是查不出東西。
SELECT
c.document_id AS documentId,
c.id AS chunkId,
c.content AS content,
d.title AS title,
d.url AS url,
(c.embedding <=> #{embedding}) AS distance
FROM kb_chunk c
LEFT JOIN kb_document d ON c.document_id = d.id
WHERE c.embedding IS NOT NULL
ORDER BY distance
LIMIT #{topK}
當去除order by後,即執行下面的sql才能查出東西(但是由於沒有topK邏輯了,不符合需求,只能靠java對數據結構再做topK)
SELECT
c.document_id AS documentId,
c.id AS chunkId,
c.content AS content,
d.title AS title,
d.url AS url,
(c.embedding <=> #{embedding}) AS distance
FROM kb_chunk c
LEFT JOIN kb_document d ON c.document_id = d.id
WHERE c.embedding IS NOT NULL
LIMIT #{topK}
更反常的事來了,輸入不同query進行查詢的話結果不一樣:
query=“重啓” ✅ 能查出來
query=“你好,怎麼用知識庫檢索?” ❌ 查不出來(返回 [])
2.排查過程
我做了幾件“確認”:
2.1確認表裏真的有embedding
select count(*) from kb_chunk; -- 2
select count(*) from kb_chunk where embedding is not null; -- 2
2.2確認向量維度一致
維度不一致會導致距離計算出問題。
SELECT vector_dims(embedding) AS dims, COUNT(*)
FROM kb_chunk
WHERE embedding IS NOT NULL
GROUP BY dims;
結果:dims=1024, count=2,同時在Java裏打印query向量維度:也是1024。所以排除“維度不一致”。
2.3確認向量裏沒有 NaN/Infinity
如果embedding裏出現NaN(非數字)或Infinity(無窮大),排序會亂。
在java中加入以下代碼做調試,發現也不存在NaN或Infinity
var list = queryEmbedding.vectorAsList();
long badCount = list.stream().filter(v -> {
double d = ((Number)v).doubleValue();
return Double.isNaN(d) || Double.isInfinite(d);
}).count();
log.debug("query='{}' dim={}, badCount={}", query, list.size(), badCount);
發現真實原因
此時其實我能隱約感覺到會不會是我的數據數量太少的原因,於是做了以下驗證查索引定義:
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'kb_chunk';
發現了下面這條內容:
CREATE INDEX idx_kb_chunk_embedding_ivfflat
ON public.kb_chunk USING ivfflat (embedding vector_cosine_ops)
WITH (lists='100')
原來是因為向量索引(ivfflat)的lists太大 + probes太小,導致“空桶”!
2.1 原因剖析
向量數據庫的查詢並不是簡單的“=”,而是“<=>”。當一定情況下會使用向量索引,這是是“近似檢索”,它不保證一定能找出候選數據。
2.1.1 ivfflat的工作方式
ivfflat 可以把它想象成下面兩步:
第一步:lists(分桶)
建索引時設置了:USING ivfflat ... WITH (lists = 100)
lists 的意思可以理解為把所有向量分成100個桶(list),每條向量會落到其中一個桶裏。當數據量很大時(比如10萬條),分成100個桶沒問題,每個桶裏都能有很多數據。但實際數據量是2條(因為是demo產品),這就會出現很誇張的情況:100個桶裏,最多隻有2個桶裏有數據,剩下98個桶是空桶(桶裏沒有任何向量)。
第二步:probes(探測桶)
查詢時數據庫不會掃完100個桶,它會先判斷“最可能相近”的幾個桶,然後只在這些桶裏找候選。這個“查幾個桶”,就是probes:
probes 越小:越快,但越容易漏。
probes 越大:越慢,但越穩。
默認 probes 往往是 1(只看 1 個桶)。
為什麼“重啓”能查到數據,而“你好…”查不到?
這就是“空桶效應”:
query=“重啓” 的向量,落到的“最相近桶”剛好是一個非空桶
→ probes 即使不大,也能在桶裏找到那條向量 → 有結果
query=“你好,怎麼用知識庫檢索?” 的向量,落到的“最相近桶”剛好是空桶
→ probes=10 只探測 10 個桶,但這 10 個桶恰好都空 → 候選集=0 → 返回 0 行
當把probes設為100時,相當於把所有桶都探測了一遍:
即使 query 先落到空桶,最終也會探測到那兩個非空桶
→ 一定能撈到候選 → 就能返回結果。
為什麼“去掉 ORDER BY”就正常?
因為去掉 ORDER BY embedding <=> query_vector 後,數據庫不需要計算距離,也就不會走 ivfflat 向量索引,查詢退化成普通的 “從表裏隨便拿幾條數據” 的行為,所以它會穩定返回 LIMIT 的結果(只要表裏有數據)。
避免踩坑
數據量很小的時候,不要把 lists 設很大(比如 N=2 還 lists=100)
如果必須用 ivfflat的話,讓 probes 足夠大(至少不遠小於 lists)
或者直接把 lists 設小(例如 1~10)
可能的歧義
有人可能會問,既然是因為算了distance,觸發了向量近似索引,可是在主句中也算了distance呀:
select ..., (c.embedding <=> #{embedding}) AS distance from kb_chunk
這其實涉及到了一些數據庫的優化。如果加上 ORDER BY distance LIMIT k 後,數據庫需要找出‘最相似的前 k 條’,這通常會觸發向量索引(ivfflat)走近似檢索路徑;而沒有 ORDER BY 時,數據庫只需要隨便返回 k 條記錄,不必做‘全局最相似’的計算,也就沒有那麼大壓力,因此不會走 ivfflat。