博客 / 詳情

返回

pgvector語義檢索踩坑:為什麼加了 ORDER BY 反而查不到數據?

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。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.