一个团队花了整整两周调试语义搜索——结果完全跑偏。嵌入向量没问题,模型没问题,数据也没问题。最后发现:他们用vector_cosine_ops建了HNSW索引,但查询时用了<->(L2距离)操作符。PostgreSQL默默放弃索引,退化成全表扫描,按欧氏距离而非余弦相似度返回结果。没有报错,没有警告,只有安静的错误答案和600倍的性能惩罚。
这是pgvector最常见的踩坑方式,而搞懂每种距离函数到底在测什么、操作符与索引如何绑定,完全可以避免。
三种距离函数,测的不是一回事
余弦距离(<=>)只关心方向,完全无视长度。两个向量只要指向同一方向,余弦距离就是0,不管它们长度是1还是1000。取值范围0到2。OpenAI、Cohere、Voyage等主流嵌入模型的输出默认归一化,这是最常见的选择。
L2距离(<->)测的是空间中的直线距离,对长度敏感。方向相同但长度不同的向量,L2距离不为零。取值范围0到无穷。适合空间坐标、传感器数据等长度本身有意义的场景。
内积(<#>,取负)返回点积的相反数,让ORDER BY按最小值优先返回最相似结果。对角度和长度都敏感。推荐系统里的最大内积搜索(MIPS)用这个。
索引与操作符的绑定陷阱
pgvector的索引创建时必须指定操作符类(operator class),这个选择把索引"焊死"在特定距离函数上:
建索引时选vector_cosine_ops,查询就必须用<=>;选vector_l2_ops,查询就必须用<->;选vector_ip_ops,查询就必须用<#>。
错配时PostgreSQL不会报错,只是不用索引。查询结果"看起来能跑",但底层已经变成全表扫描加暴力计算。那个600倍性能暴跌的团队,正是栽在这个沉默的降级上。
更隐蔽的是:余弦相似度和余弦距离互为补数(cosine similarity = 1 - cosine distance),但pgvector的<=>直接返回距离。有人误以为要手动转换,在查询里写1 - (embedding <=> query),结果又触发函数计算,索引失效。
怎么选对距离函数
先看你的嵌入模型输出是否归一化。OpenAI的text-embedding-3系列、Cohere的embed-v3、Voyage的系列模型,输出都是单位长度,余弦距离和内积在此等价,但<=>的语义更直观。
如果你自己训的模型没做归一化,或者需要保留向量长度信息(比如同时编码强度和方向),L2或内积更合适。推荐系统常用内积,因为用户向量和物品向量的模长可以编码交互频率或流行度。
一个快速验证方法:取几条已知相关的查询-文档对,分别用三种距离函数排序,看哪种把正确答案顶在前面。这比盲信"余弦适合语义搜索"的教条更可靠。
性能排查清单
遇到pgvector查询慢,先执行EXPLAIN ANALYZE。如果看到Seq Scan而不是Index Scan using hnsw_index,检查三点:操作符与索引类是否匹配、是否用了强制类型转换(如::vector把维度搞错)、WHERE子句是否有可下推的条件。
索引构建参数也影响召回率和速度。m控制图连接数(默认16),ef_construction控制建图时的搜索范围(默认64)。向量维度高、数据量大时,适当调高这两个值能减少"明明很近却没搜到"的情况。
那个调试两周的团队最后把查询操作符从<->改成<=>,延迟从秒级降到毫秒级。他们的复盘文档里写了一句:「最昂贵的bug,是那些不报错就给你错误答案的。」
你现在用的pgvector查询,EXPLAIN出来是索引扫描还是全表扫描?
热门跟贴