Vector Indexes and Store Selection: HNSW vs. Inverted File, Quantization to Save Memory, Filtered Queries and Multi-Tenant Isolation
Push vector search from working to holding up under load: understand how the two index structures are built and how their parameters affect recall and latency, compress memory with half-precision and binary quantization, solve the trickiest problem — filtered queries — then give selection criteria between pgvector and a dedicated vector store.
今日目标
- 能说清分层可导航小世界图与倒排文件两种索引的构建方式,以及各自要调的参数分别影响什么
- 能解释带过滤的向量查询为什么会漏召回,并说出迭代扫描与预过滤两种应对的适用条件
- 能给出一份选型判据,说明什么规模和什么约束下应该留在 PostgreSQL,什么时候该上专用向量库
昨天你已经能把文档切成合适的块了。今天解决块变多之后的事:一万块、一百万块的时候,检索还快不快、还准不准、加了条件之后还对不对。读完并做完实验之后,回到页面顶部把这三条目标逐一勾掉。
小白版讲解
三十万本书不可能一本本翻
第二天你写的那条查询,本质上是把库里每一行的向量都跟问题向量算一次距离,然后排序取前十。三十篇语料切出几百块,这么干毫无问题,眨眼就出来了。
但这个做法的成本跟库的总量成正比:行数翻十倍,扫的时间就翻十倍。我们的实验里,两万行、384 维、单机 Docker,一次全表比对的中位延迟是 5.3 毫秒——听着还行,可这已经是走索引那 0.8 毫秒的六倍,而且这条线是直的:两百万行时它会变成半秒,用户那边就是一次明显的卡顿。
所以从这里开始,检索换了个目标:不再追求"一定找到最近的十条",而是"在可接受的时间内,大概率找到最近的十条"。这类做法统称近似最近邻(approximate nearest neighbor)。近似两个字是今天的全部前提——你从此拿到的每一组结果,都可能少了几条本该在里面的。
衡量"少了多少"的指标叫召回率:真值的前十条里,索引给你的前十条命中了几条。真值怎么来?把索引关掉全表扫一遍,那就是标准答案。它是整个调参工作的地基——没有真值,你调的所有参数都只是在改变一个你看不见的东西。今天实验的第一个练习点就是它:如果算真值的那条查询自己也走了索引,量出来的召回率会永远是 100%,而你毫无察觉。
这套量法本身只有十几行,今天所有的表格都由它产出:
// 真值:把索引关掉,全表扫一遍。只有这样量出来的才是召回率,
// 否则你是在拿索引跟索引自己比,结果永远接近 100%
export async function groundTruth(sql, queryVector, k = 10) {
return sql.begin(async (tx) => {
await tx.unsafe('set local enable_indexscan = off')
await tx.unsafe('set local enable_bitmapscan = off')
const rows = await tx`
select chunk_id from chunk_embeddings
order by embedding <=> ${queryVector}::vector limit ${k}
`
return rows.map((row) => row.chunk_id)
})
}
// 分母用真值条数,不要写死 k:带过滤时符合条件的行本来就可能不足 k 条
export function recallAt(returned, truth) {
if (truth.length === 0) return 1
const truthSet = new Set(truth)
return returned.filter((id) => truthSet.has(id)).length / truth.length
}async def ground_truth(conn, query_vector, k: int = 10) -> list[str]:
"""真值:关掉索引扫描,全表算一遍。拿索引结果当真值,召回率永远接近 100%"""
async with conn.transaction():
await conn.execute("SET LOCAL enable_indexscan = off")
await conn.execute("SET LOCAL enable_bitmapscan = off")
rows = await conn.fetch(
"SELECT chunk_id FROM chunk_embeddings ORDER BY embedding <=> $1 LIMIT $2",
query_vector,
k,
)
return [row["chunk_id"] for row in rows]
def recall_at(returned: list[str], truth: list[str]) -> float:
"""分母用真值条数,不要写死 k:带过滤时符合条件的行可能不足 k 条"""
if not truth:
return 1.0
hit = len(set(returned) & set(truth))
return hit / len(truth)回到图书馆。近似检索就是承认一件事:你不可能为了找十本书而把整个馆翻一遍,你得靠目录。而目录怎么组织,直接决定了你会漏掉什么。
HNSW:一张分层的熟人地图
第一种目录叫分层可导航小世界图(hierarchical navigable small world,业内一律简称 HNSW)。名字很唬人,拆开只有两个想法。
第一个想法是小世界图:给每个向量连上若干个较近的邻居,整个库就成了一张网。找最近邻时不用扫全表,从任意一点出发,每次挪到"离目标更近的那个邻居",像顺着人脉打听,几十步就能摸到目标附近。
第二个想法是分层。只有一张网的话,从库的这头走到那头要走很久。所以再往上叠几层稀疏的网:最上层点少、连得远,像城际高铁;越往下点越密、连得越近,像市内公交。查询从顶层入口进来,先坐高铁到大致区域,再层层下沉精找。这和跳表是同一个套路。
pgvector 把它做成了两个建图参数加一个查询参数:
-- 建索引:m 是每个点在每一层最多连多少个邻居(默认 16),
-- ef_construction 是建图时每个点的候选邻居列表有多大(默认 64)
CREATE INDEX ON chunk_embeddings
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
-- 查询:ef_search 是搜索时动态候选列表的大小(默认 40)
SET hnsw.ef_search = 100;三个参数管三件事,别搞混:m 决定图有多密,密了召回高但索引大、建得慢;ef_construction 决定建图时找邻居有多认真,只影响建索引这一次;ef_search 是唯一一个能逐次查询调整的。前两个改了要重建索引,最后一个立刻生效。
取舍曲线长什么样?两万行的实验里,ef_search 从 10 涨到 400,召回率从 69.0% 爬到 98.0%,中位延迟从 0.6 毫秒涨到 2.4 毫秒(建图带随机性,你自己跑会差一两个百分点,看趋势不看小数点)。注意形状:前半段很划算,后半段很不划算——从 10 调到 100 多花 0.5 毫秒买回 26 个百分点,从 200 调到 400 多花 0.8 毫秒只买回 1 个百分点。调参的活儿到这儿就是一句话:找那个膝盖点。
IVFFlat:先分区再进区里翻
第二种目录叫倒排文件索引(inverted file,简称 IVFFlat),思路完全不一样,而且更好懂。
建索引时先对所有向量做一次 k 均值聚类,分成 lists 个簇,各记住自己的中心点。查询时先把问题向量跟所有中心点比一遍,挑最近的 probes 个簇,只在这几个簇里扫。相当于图书馆先按学科分成若干书库,你先判断该去哪几个库,再在库里逐本翻。
-- lists 的起点:不超过 100 万行取 rows / 1000,超过取 sqrt(rows)
CREATE INDEX ON chunk_embeddings
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 20);
-- probes 的起点:sqrt(lists)。默认值是 1,几乎肯定不够
SET ivfflat.probes = 10;分工很干净:lists 决定分多细,建索引时定死;probes 决定查的时候看几个簇,随时能改。probes 调到等于 lists 就退化成全表扫描——实验里正好跑出这个结果:probes 从 1 到 20,召回从 60.0% 一路到 100.0%,延迟从 0.5 毫秒涨到 5.3 毫秒,而全表扫描本身就是 5.3 毫秒。
这里有个新手必踩的坑:probes 默认是 1。建完索引什么都不调,查询只看一个簇,召回可能只有五成多。IVFFlat 建完必须紧接着设 probes,这不是优化,是让它能用。
两种怎么选?实测建索引开销是 HNSW 1.5 秒 / 38.8 MB,IVFFlat 0.3 秒 / 31.3 MB:IVFFlat 建得快、占得小,但同等召回下延迟更高。还有一条更要紧的差别:IVFFlat 的簇由建索引那一刻的数据决定,持续写入后聚类会失真、需要定期重建,HNSW 是增量插入的图,没这个问题。所以数据静态、内存紧张选 IVFFlat,写入频繁、查询质量优先选 HNSW。
量化:把书换成缩微胶片
索引省时间,量化省空间。384 维的单精度向量在 pgvector 里占 4 × 维数 + 8 字节,实测每行 1540 字节,两万行的表 36.5 MB,HNSW 索引 38.8 MB——索引比表还大,这在向量检索里是常态。上到千万行,内存就是账单的大头。
第一档是半精度(halfvec):每一维从 4 字节压到 2 字节。实测每行 776 字节,索引从 38.8 MB 降到 22.2 MB,同样 ef_search = 40 下召回是 90.5% 对 92.0%——两者的差别落在重跑的噪声里,可以认为没有损失。它还顺带解一个硬限制:vector 建索引最多 2000 维,halfvec 到 4000 维,bit 到 64000 维——用 3072 维模型的人只能走这条路。
-- 半精度:表达式索引,不用改列的类型
CREATE INDEX ON chunk_embeddings
USING hnsw ((embedding::halfvec(384)) halfvec_cosine_ops);
SELECT chunk_id FROM chunk_embeddings
ORDER BY embedding::halfvec(384) <=> $1::halfvec(384) LIMIT 10;第二档是二值量化:每一维只留符号,正数记 1、其余记 0,384 维压成 384 个比特,实测每行 56 字节,索引降到 6.6 MB,比原来小了将近六倍。代价当然也大,所以标准用法从来不是直接拿它出结果,而是用它粗筛一大批,再用原始向量在这一小批里精排:
SELECT chunk_id FROM (
SELECT chunk_id, embedding FROM chunk_embeddings
ORDER BY binary_quantize(embedding)::bit(384) <~> binary_quantize($1)::bit(384)
LIMIT 500
) coarse
ORDER BY embedding <=> $1 LIMIT 10;加一句 WHERE,结果就少了
前面都是热身,这一节才是今天最容易在生产上翻车的地方。
考虑一个再普通不过的需求:只在某个部门、某个租户、某个时间段的文档里检索。SQL 加个条件就行。但你会发现一件怪事——明明库里有几百条符合条件的行,查询却只返回了一两条。
原因 pgvector 文档写得很直白:近似索引的过滤是在索引扫描之后做的。 索引先按距离取回 ef_search 个候选(默认 40 个),然后才拿 WHERE 去筛这 40 个。符合条件的行只占全表 1% 的话,40 个候选里平均只剩 0.4 个能活下来。
我们把这个场景真的构造了出来:两万行里给一个租户分了 200 行(正好 1%),带着租户条件检索。默认配置下平均每次只返回 0.5 条,召回率 5.0%。不是数据库坏了,也不是数据没进去——把索引关掉全表扫描立刻就能返回完整的十条。
pgvector 从 0.8.0 起给了一个开关,叫迭代扫描:候选被过滤掉太多时,自动回到索引里继续多扫一些,直到凑够为止。
-- 严格顺序:结果严格按距离排好,召回略低
SET hnsw.iterative_scan = strict_order;
-- 宽松顺序:允许结果的距离顺序略有出入,换更高的召回
SET hnsw.iterative_scan = relaxed_order;同一批查询,开 relaxed_order 后召回从 5.0% 回到 89.5%,开 strict_order 回到 77.5%,代价是中位延迟从 0.7 毫秒涨到 6 毫秒上下。这就是本课一直强调的三笔账:指标涨 84 个百分点,延迟涨约 9 倍,钱没涨。值不值取决于你的过滤条件有多苛刻。
在应用侧,这两个开关都要用 SET LOCAL 打进事务,而不是全局设置,否则不带过滤的普通查询也会跟着变慢:
// 只在这一次事务里生效;不带过滤的查询不该被拖慢
export async function searchInTenant(sql, queryVector, tenantId, k = 10) {
return sql.begin(async (tx) => {
await tx.unsafe('set local hnsw.ef_search = 40')
await tx.unsafe('set local hnsw.iterative_scan = relaxed_order')
return tx`
select chunk_id, embedding <=> ${queryVector}::vector as distance
from chunk_embeddings
where tenant_id = ${tenantId}
order by distance
limit ${k}
`
})
}async def search_in_tenant(conn, query_vector, tenant_id: str, k: int = 10):
"""只在这一次事务里生效;不带过滤的查询不该被拖慢"""
async with conn.transaction():
await conn.execute("SET LOCAL hnsw.ef_search = 40")
await conn.execute("SET LOCAL hnsw.iterative_scan = relaxed_order")
return await conn.fetch(
"""
SELECT chunk_id, embedding <=> $1 AS distance
FROM chunk_embeddings
WHERE tenant_id = $2
ORDER BY distance
LIMIT $3
""",
query_vector,
tenant_id,
k,
)多租户:三种隔离,三种账单
带过滤检索最常见的形态是多租户:一套系统服务很多客户,每个客户只能查到自己的东西。三种做法,成本结构完全不同。
第一种是加过滤字段,就是上一节那套。改动最小,但除了漏召回还有一个更隐蔽的问题:所有租户共用同一张图,A 租户塞进来一百万条数据,会实实在在拖慢 B 租户的检索质量和速度——B 的候选名额被 A 占掉了。这一点 pgvector 文档专门提醒过。
第二种是给高频租户建部分索引:索引只包含这个租户的行。实验里给那个 1% 的租户单独建了一条,索引只有 408 KB、建索引 0.1 秒,不开迭代扫描召回就是 100%,中位延迟 0.4 毫秒,三个指标全面胜出。代价在别处:索引谓词里不能用绑定参数,每个租户都要单独一条 DDL,租户上千就是上千条索引。
-- 部分索引:条件只能是常量,所以一个租户一条 DDL
CREATE INDEX ON chunk_embeddings
USING hnsw (embedding vector_cosine_ops) WHERE (tenant_id = 't07');第三种是列表分区:按租户把表拆成多个分区,各有各的索引。这是 pgvector 文档给多租户隔离推荐的做法,隔离最彻底,删租户就是删分区,代价是复杂度搬到了 DDL 和运维上。
判据很简单:租户少且稳定用部分索引;租户多但每个数据量都不大,用过滤字段加迭代扫描;有大客户会挤掉小客户,就上分区。 三条不冲突,很多系统混着用——头部大客户单独分区,长尾共用一张表。
一句边界:今天讲的是"怎么让带条件的检索不漏","这个用户到底有没有权限看这一条"是另一件事,涉及权限来源、失效与增量同步,第 13 天专门处理。
什么时候该把向量搬出 PostgreSQL
先说结论:绝大多数团队的第一版都应该留在 PostgreSQL,理由不是它的向量检索多强,而是它省掉的东西太多——事务、备份、时间点恢复、权限、跟业务表 JOIN、你已经熟悉的运维工具全是现成的。多一个数据库,就多一份同步、一份一致性问题、一份半夜被叫起来的可能。
真该考虑搬走时看四条线,任意一条明确越界就值得重新评估:
- 数据量。 单表向量进入千万级、且必须常驻内存时,专用库在内存布局与换页上的优化开始显出差距。判据不是"我有多少行",而是"索引还塞不塞得进内存"。
- 写入频率。 高频增删改会让 IVFFlat 的聚类失真、让 HNSW 的图持续膨胀。分钟级更新的流比天级批量导入更需要专用库的增量维护。
- 过滤复杂度。 最实际的一条:过滤条件不是一个租户字段而是十几个属性的任意组合时,部分索引和分区都排列组合不过来,Qdrant 这类库把过滤做进了索引结构本身,天生更适合。
- 团队运维能力。 反过来的一条:没人愿意长期照看第二个数据库,前三条再成立也别搬。
还有一条常被忽略的路:不搬走,也不是只有一种检索。 关键词索引、向量索引和第 9 天要讲的重排本来就该并存,很多"向量检索不够用"的问题真正的解法在第 9 天而不是换库。至于用哪个 embedding 模型、开多少维,是第 2 天定下的事,复习看 D2。
源码导读
动手实验
动手之前先在实验目录里 docker compose up -d 起库。这一天没有内存版可退——要量的就是数据库索引本身,用假实现替代等于什么都没量到,所以连不上库时脚本会提示你怎么起库然后退出。MOCK=1 只替换 embedding 这一个外部依赖,不需要网络也不需要 key。
starter/ 挖了四个练习点,顺序有讲究:先把召回率算对,再让真值真的走全表扫描,然后才轮到调参和修漏召回。前两个不做,后两个的数字全是假的——这本身就是今天最想让你记住的方法论。
- 起库,把两万行数据灌进去,先看一眼全表暴力比对的延迟,记住这个数——它是后面所有对照的分母。
- 补完召回率计算和真值查询,重跑一遍,看到 HNSW 的召回率从"全是 100%"变成一条随
ef_search上升的真实曲线。 - 把 IVFFlat 的
lists从写死的 1 改成按行数算,再扫一遍probes,对比两种索引在同等召回下的延迟差距。 - 跑带租户过滤的那一组,先看到平均只返回零点几条的现场,再打开迭代扫描把它修好,记录延迟涨了多少倍。
- 把半精度与二值量化的空间、速度、召回三列抄进最后的选型备忘,每条结论后面都写清楚它是在多少行数据上成立的。
面试题
今天 4 道题在下方题库区,侧重近似最近邻索引的原理与调参、过滤查询的漏召回、向量库选型。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。
检查清单与明日预告
- 能说清分层可导航小世界图与倒排文件两种索引的构建方式,以及各自要调的参数分别影响什么
- 能解释带过滤的向量查询为什么会漏召回,并说出迭代扫描与预过滤两种应对的适用条件
- 能给出一份选型判据,说明什么规模和什么约束下应该留在 PostgreSQL,什么时候该上专用向量库
- 能分清"结果条数不够"和"条数够了但排序不对"这两种漏召回,并说出各自该用哪把扳手
- 实验的 5 条验收标准全部通过,选型备忘里每条结论都标了数据规模
- 4 道面试题不看要点也能答出至少 3 道
明天(D6)把战场换到生成这一侧:材料取回来之后,上下文该怎么排、引用该怎么标、什么时候必须拒答,以及怎么把回答流式吐出去。为什么是这个顺序?因为今天之后,"该取的取得到"已经有了可量化的保障,接下来才轮得到追究"取到了却答错"。检索侧的问题用生成侧的手段补不回来,反过来也一样——先把两边的责任划清楚,第 8 天做评估时才知道每个指标该找谁负责。
Interview questions
How do you choose between an HNSW index and an IVFFlat index? Give one scenario that forces each choice, and name the parameter you would tune first in each.分层可导航小世界图和倒排文件索引你会怎么选?各说一个必须选它的场景,以及各自最该调的参数。
Common in ChinaCommon overseasIntermediate#vector-index#hnsw#ivfflatHow to reason about it · think before answering
- The differentiator is not describing both structures, it is naming the condition that forces one over the other. Saying 'HNSW is faster, IVFFlat is cheaper' is what everyone says.
- Describe the structures in one line each: HNSW is a layered neighbour graph you navigate from sparse upper layers down to dense lower ones; IVFFlat clusters vectors into lists and only scans the lists closest to the query.
- Map the knobs: HNSW builds with m and ef_construction and queries with ef_search; IVFFlat builds with lists and queries with probes. Tune the query-side knob first, because it needs no rebuild and is the only one you can still move after launch.
- Give two forcing scenarios in opposite directions. Minute-level write traffic with tight memory and a short build window forces IVFFlat, since an HNSW graph keeps growing and is expensive to rebuild. A largely static corpus with a hard latency SLA forces HNSW, since it hits the same recall at lower latency.
- Add the operational detail people forget: IVFFlat clusters reflect the data at build time, so recall degrades silently as the distribution drifts and you need a scheduled rebuild. HNSW avoids that but its index is often larger than the table.
- Expected follow-up: what are the defaults? probes is 1 and ef_search is 40. Volunteer that leaving probes at 1 means scanning a single list, which is the single most common IVFFlat mistake.
分析过程 · 先想清楚再作答
- 这题的区分度不在能不能背出两种结构,而在你会不会给出触发条件。只说「HNSW 快、IVFFlat 省内存」的人一抓一大把,面试官等的是「什么情况下我必须选另一个」。
- 先用两句话把结构说清:HNSW 是分层的邻居图,查询从稀疏的上层跳到稠密的下层,逐步逼近;IVFFlat 是先聚类成若干个列表,查询时只在最近的几个列表里扫。一个是图上导航,一个是分区搜索。
- 再把参数对应上去:HNSW 建图有 m 与 ef_construction,查询有 ef_search;IVFFlat 建索引有 lists,查询有 probes。**先调查询侧参数**,因为它不用重建索引、能逐次查询调整,是唯一一个上线之后还能动的旋钮。
- 给两个反向的必须场景:数据分钟级高频写入、且内存和建索引窗口都紧张时必须选 IVFFlat,因为 HNSW 的图会持续膨胀、重建代价高;反过来,数据相对静态、查询延迟有硬性 SLA 时必须选 HNSW,因为同等召回下它的延迟更低。
- 补一条容易被忽略的工程细节:IVFFlat 的聚类是建索引那一刻的数据决定的,数据分布漂移之后召回会悄悄下滑,所以它需要一条定期重建的运维流程;HNSW 没有这个包袱,但它的索引往往比表本身还大。
- 可预期的追问:probes 和 ef_search 的默认值分别是多少?答 1 和 40,并且要主动说出 IVFFlat 默认 probes = 1 意味着只看一个列表,建完索引不设 probes 基本等于没调过——这是新手最常见的事故。
Key points
- HNSW is a layered neighbour graph; IVFFlat clusters first and scans a subset of lists. HNSW favours query quality, IVFFlat favours build cost and memory.
- Tune the query-side knob first: ef_search for HNSW, probes for IVFFlat. Neither needs a rebuild.
- Heavy write traffic with tight memory and build windows points to IVFFlat; a static corpus with a hard latency SLA points to HNSW.
- IVFFlat clusters drift with the data and need scheduled rebuilds; HNSW does not, but its index is often larger than the table.
- Know the defaults: probes 1, ef_search 40. Leaving probes at 1 wastes the index.
答题要点
- HNSW 是分层邻居图,IVFFlat 是先聚类再局部扫描;前者查询质量优先,后者建索引与内存开销优先。
- 先调查询侧参数:HNSW 调 ef_search,IVFFlat 调 probes,两者都不需要重建索引。
- 高频写入、内存与建索引窗口紧张选 IVFFlat;数据相对静态、延迟有硬性要求选 HNSW。
- IVFFlat 的聚类会随数据漂移失真,需要定期重建;HNSW 没这个问题但索引常常比表还大。
- 默认值要记住:probes 是 1、ef_search 是 40,建完索引不调 probes 等于没用上索引的能力。
Why does a vector search with a WHERE clause return fewer results than expected, and what are the fixes and their costs?为什么加了 WHERE 条件的向量检索会漏结果?有哪几种修法,代价分别是什么?
Common in ChinaCommon overseasDeep dive#filtering#iterative-scan#recallHow to reason about it · think before answering
- This is the question that separates people who ran a demo from people who ran this in production. The tell is whether you distinguish missing rows from mis-ordered rows.
- State the mechanism in one sentence: with approximate indexes, filtering is applied after the index scan. The index first collects ef_search candidates by distance, and only then applies the WHERE clause to that batch.
- Do the arithmetic out loud: a condition matching 1% of rows against a default candidate list of 40 leaves well under one row on average. That is why the query looks broken even though the rows exist.
- Split the failure into two kinds. Too few rows returned is one; enough rows but the wrong ones ranked first is the other. They have different fixes, and conflating them signals inexperience.
- Fix one is iterative scanning, available since pgvector 0.8.0: when too many candidates are filtered out, keep scanning more of the index until enough results are found. Strict ordering keeps exact distance order, relaxed ordering trades slight reordering for better recall, and both cost latency.
- Fix two is making the filter apply first: a plain index on the filter column for highly selective conditions, a partial index when there are only a few distinct values, list partitioning when there are many. The costs are losing the approximate speedup, index count exploding per value, and DDL plus operational complexity.
- Expected follow-up: how do you pick? Check the returned row count first. Too few means iterative scanning; enough rows with low recall means raising probes or ef_search, or switching to pre-filtering.
分析过程 · 先想清楚再作答
- 这题是本天的核心,也是最能筛掉「只跑过 demo」的人的一题。题眼在「漏」这个字:能不能说清楚漏的是条数还是排序,直接决定你被归到哪一档。
- 先讲机制,一句话就够:近似索引的过滤发生在索引扫描之后。索引先按距离取回 ef_search 个候选,然后才拿 WHERE 去筛这一批。条件命中率越低,活下来的越少——命中 1% 的条件配默认的 40 个候选,平均只剩零点几条。
- 然后把漏召回拆成两类,这是拿分点:一类是**结果条数不够**,十条只给了一两条;另一类是**条数够但排序不对**,十条都在只是排错了。两类的修法完全不同,混为一谈说明没真跑过。
- 修法一是迭代扫描(pgvector 0.8.0 起):候选被过滤掉太多时自动回索引里继续扫,直到凑够。它只解决第一类。两种模式的取舍要说清楚——严格顺序保证结果按距离排好,宽松顺序允许略微乱序换更高召回,代价都是延迟明显上升。
- 修法二是预过滤,即让过滤条件先生效:条件很挑剔时给过滤列建普通索引走精确检索,取值只有少数几个时建部分索引,取值很多时按值做列表分区。代价分别是失去近似索引的加速、索引数量随取值爆炸、以及 DDL 与运维复杂度上升。
- 可预期的追问:怎么判断该用哪一种?给一条可执行的判据——先看返回条数够不够。不够是第一类,先试迭代扫描;够了但召回低是第二类,只能加大 probes 或 ef_search,或者干脆改成预过滤。
Key points
- With approximate indexes the filter runs after the index scan, so a selective condition wipes out most candidates and the query returns too few rows.
- There are two failure modes: too few rows, and enough rows in the wrong order. Always check the returned count first.
- Iterative scanning fixes only the first. Strict ordering preserves distance order, relaxed ordering gives better recall, and both raise latency noticeably.
- Pre-filtering is the alternative: index the filter column for exact search, use a partial index for a few distinct values, partition by value for many. Costs are losing the approximate speedup, index sprawl, and operational complexity.
- The second failure mode is only fixed by raising probes or ef_search; iterative scanning does nothing for it.
答题要点
- 近似索引的过滤发生在索引扫描之后,条件命中率低时候选几乎被筛光,所以返回条数不够。
- 漏召回分两类:条数不够,和条数够但排序不对。判断顺序永远是先看返回条数。
- 迭代扫描只修第一类,严格顺序保序、宽松顺序召回更高,代价是延迟明显上升。
- 预过滤是另一条路:过滤列建索引走精确检索、取值少建部分索引、取值多按值分区,代价依次是失去索引加速、索引数量爆炸、运维复杂度上升。
- 第二类只能靠加大 probes 或 ef_search,迭代扫描对它完全无效。
If you switch your vectors from full precision to half precision or binary quantisation, how do you verify that recall has not dropped materially?把向量从全精度换成半精度或二值量化,你会用什么方法确认召回没有明显下降?
Common in ChinaCommon overseasIntermediate#quantization#evaluation#recallHow to reason about it · think before answering
- The question looks like it is about quantisation, but it is really about whether you know how to evaluate. Answering 'try a few queries and eyeball it' fails immediately.
- Pin down ground truth first: it must come from an exhaustive scan with the index disabled. Using index results as ground truth is the classic self-deception, because recall then looks close to 100% no matter what you changed.
- Give the procedure: fix a query set of at least a few dozen covering short and long queries across topics, compute ground truth at full precision, rerun with the quantised representation, and report recall at k. Report index size, build time, and median plus p95 latency alongside it, because recall alone is not a decision.
- Add the judgement rule: quantisation loss depends on your vector distribution, so published numbers do not transfer. Sparse vectors suffer badly under binary quantisation because only the sign bit survives and zeros collapse together.
- Land on something actionable: half precision is usually near lossless and raises the indexable dimension ceiling from 2000 to 4000, so it is a safe first step. Binary quantisation loses real recall and should be used as a cheap first pass, re-ranked with the original vectors over a wider candidate window.
- Expected follow-up: how much loss is acceptable? It depends on what comes next. With a re-ranker downstream, a couple of points off first-stage recall is usually invisible; if retrieval feeds the prompt directly, one point means one more unanswerable question per hundred. Tie the threshold to a product metric, not to a number you made up.
分析过程 · 先想清楚再作答
- 这题表面问量化,实际问的是你会不会做评估。只回答「跑几个问题看看结果对不对」的人会被直接判为没做过——面试官想听的是一套可复现的量法。
- 先把真值这件事说死:真值必须来自暴力全量比对,也就是把索引关掉、全表算距离取前 k。拿索引结果当真值是最常见的自欺,因为那样量出来的召回永远接近 100%,你会以为量化无损。
- 然后给流程:固定一批查询(几十条起步,覆盖长短查询和不同主题),先用全精度算出真值,再换量化重跑,计算召回率@k。同时记录三件事——索引大小、建索引耗时、查询延迟的中位数与 p95,只报召回是不够的。
- 补一条判据:量化损失有多大取决于向量分布,别人的数字不能抄。稀疏向量对二值量化尤其不友好,因为二值化只保留符号位,零和负数会被压成同一个值,信息几乎被抹平。所以换方案必须在自己的数据上重新量一次。
- 结论要给可操作的建议:半精度通常近乎无损,还能把建索引维度上限从 2000 提到 4000,是默认可以先上的一档;二值量化损失明显,标准用法是拿它粗筛一批候选,再用原始向量在这一小批里精排,粗筛窗口越宽召回补得越多、延迟也越高。
- 可预期的追问:召回掉了多少算可以接受?答这取决于下游——后面还有重排时,粗排召回掉两三个点通常无感;如果检索结果直接进提示词,掉一个点就意味着每一百次回答里多一次缺材料。要把这个判断挂到业务指标上,而不是拍一个阈值。
Key points
- Ground truth must come from an exhaustive scan with indexes disabled; using index output as truth pins recall near 100%.
- Run one fixed query set before and after, report recall at k together with index size, build time and latency percentiles.
- Quantisation loss depends on your own vector distribution, so measure it on your data instead of quoting benchmarks.
- Half precision is usually near lossless and raises the indexable dimension limit from 2000 to 4000, making it a safe default.
- Binary quantisation loses real recall; use it as a cheap first pass and re-rank with the original vectors over a wider window.
答题要点
- 真值必须来自关掉索引的暴力全量比对,拿索引结果当真值会让召回永远接近 100%。
- 固定一批查询,量化前后跑同一批,报召回率@k,同时报索引大小、建索引耗时和延迟分位数。
- 量化损失取决于向量分布,别人的数字不能抄,必须在自己的数据上重新量。
- 半精度通常近乎无损,还能把索引维度上限从 2000 提到 4000,可以作为默认第一档。
- 二值量化损失明显,正确用法是粗筛加原始向量重排,粗筛窗口越宽召回补得越多、延迟越高。
When should you move your vectors out of PostgreSQL into a dedicated vector database? Give measurable triggers, and also make the case for staying.什么时候应该把向量搬出 PostgreSQL?给出可量化的触发条件,也说说不该搬的理由。
Common in ChinaCommon overseasIntermediate#vector-database#architecture#trade-offsHow to reason about it · think before answering
- This tests engineering judgement, not tooling preference. Opening with 'dedicated vector databases are better' invites follow-ups you cannot answer.
- State the default position and justify it: keep the first version in PostgreSQL, because transactions, backups, point-in-time recovery, permissions, joins with business tables and the tooling your team already knows all come free. A second datastore adds synchronisation, a consistency surface and an on-call burden that selection documents rarely price in.
- Then give four measurable triggers: data volume (the test is whether the index still fits in memory, not the raw row count), write frequency (minute-level streaming updates distort clusters and inflate graphs), filter complexity (arbitrary combinations of a dozen attributes defeat both partial indexes and partitioning), and operational capacity.
- Expand on filter complexity, because it is most often the real reason: dedicated vector databases push filtering into the index structure instead of applying it after the scan, which is a mechanical advantage rather than a reputational one.
- Volunteer the alternative people skip: many 'vector search is not good enough' problems are actually solved by hybrid retrieval plus re-ranking, not by a new database. Add the keyword path and a re-ranker first, then decide.
- Expected follow-up: how would you migrate? Dual-write, compare recall and latency on shadow traffic, shift read traffic gradually, and only then retire the old path. Stop at any step where the metrics regress.
分析过程 · 先想清楚再作答
- 这题考的是工程判断,不是技术偏好。开口就说「专用向量库更专业」的人会被追问到答不上来;面试官想看的是你有没有把迁移成本算进去。
- 先给默认立场并给出理由:第一版留在 PostgreSQL,因为事务、备份、时间点恢复、权限、跟业务表 JOIN 和现成的运维工具全是白送的。多一个数据库就多一份同步、一份一致性问题、一份值班负担,这些成本很少被写进选型文档。
- 然后给四条可量化的触发线:数据量(判据不是行数而是索引还塞不塞得进内存)、写入频率(分钟级流式更新会让聚类失真、让图持续膨胀)、过滤复杂度(十几个属性的任意组合让部分索引和分区都排列组合不过来)、团队运维能力(没人愿意长期照看第二个数据库,前三条再成立也别搬)。
- 第三条要展开一点,因为它最常是真正的原因:专用向量库把过滤做进了索引结构本身,而不是扫完索引再筛,所以在复杂过滤下天然占优。把这一点说出来,说明你理解的是机制而不是口碑。
- 还要主动给一条常被忽略的替代路径:很多「向量检索不够用」的问题,真正的解法是混合检索加重排,而不是换数据库。先把关键词一路加回来、把重排接上,再决定要不要搬——顺序搞反了会白搬一次。
- 可预期的追问:真要搬怎么迁?答分三步——先双写并在影子流量上比对两边的召回与延迟,再把读流量按比例切过去,最后才停掉旧路径。中间任何一步指标不达标就停下,这比一次性切换安全得多。
Key points
- Default to staying in PostgreSQL: transactions, backups, recovery, permissions, joins and familiar tooling are free, and a second store adds sync and on-call cost.
- Trigger one is data volume, measured by whether the index still fits in memory rather than by row count.
- Trigger two is write frequency: minute-level streaming updates distort clusters and inflate graphs.
- Trigger three is filter complexity: dedicated stores push filtering into the index structure, a mechanical advantage under complex predicates.
- Trigger four cuts the other way: without people to run a second database, do not move even if the first three hold. Often hybrid retrieval plus re-ranking is the real fix.
答题要点
- 默认留在 PostgreSQL:事务、备份、恢复、权限、JOIN 和现成运维都是白送的,多一个库就多一份同步与值班成本。
- 触发线一是数据量,判据是索引还塞不塞得进内存,而不是行数本身。
- 触发线二是写入频率,分钟级流式更新会让聚类失真、让图持续膨胀。
- 触发线三是过滤复杂度,专用库把过滤做进索引结构,复杂过滤下有机制上的优势。
- 触发线四反过来看:没有长期运维第二个数据库的人手,前三条成立也不该搬;很多问题的真正解法是混合检索加重排。
Comments
Sign in to join the discussion
No comments yet — be the first.