最近在做一个知识库问答项目,用的Milvus存了大概200万条384维的向量,文本是中文长文档切块后过bge-base的embedding。现在问题是:用测试集跑出来的Recall@10只有65%左右,但单独用向量检索看相似度,感觉结果还挺相关的。我试过调nprobe和efSearch,也换过距离算法(L2换IP),效果变化不大。目前怀疑是不是切块策略有问题——之前是按固定512字符切的,有些语义完整的段落被拆开了。另外也考虑过是不是embedding模型本身不够强,但暂时没资源换更大的模型。想请教下各位,除了这些常规调参,还有哪些方向值得排查?比如索引参数(HNSW的M和efConstruction)对召回的影响大吗?或者有没有必要做query改写?先谢谢了。
向量数据库召回率上不去,除了调参还能从哪些方向排查?
全部回复
共 48 条说实话你这切块512字符确实容易出问题,中文语义单元经常比这短,尤其长文档里段落边界被硬切后,向量相似度看着行但召回就是上不去。我建议先按语义段落或句子边界重切,设个min/max长度范围,再试试用重叠窗口保留上下文,这比换模型成本低多了。另外索引那边,HNSW如果M调太大反而容易过拟合,efConstruction对召回影响不如查询时efSearch明显,你可以先固定M在16-32,拿1000条query做A/B测。还有Milvus的metricType最好跟训练embedding时用的相似度对齐,bge系列一般配cosine,你换IP不一定对路。
你这个怀疑方向我觉得挺对的,固定512字符切块确实容易把语义边界切碎,尤其中文长文档里那种“总-分”结构,切完以后每块都像半句话,向量相似度看着高但召回不精准。我之前遇到过类似情况,后来改成按段落或者语义完整性来切,比如用句号、分号做软边界,再配合滑动窗口重叠一部分字符,Recall直接涨了七八个点。另外你提到embedding模型,bge-base在中文上其实不弱,但如果你切块后的文本长度分布差异很大,比如有些块只有几十字,有些上千字,那模型输出的向量质量会很不稳定,建议先统计一下块长度分布,考虑统一到256-512这个区间再试。索引参数那边,HNSW的M和efConstruction确实影响召回,但200万条数据量不算大,你可以试试M从16调到32,efConstruction从200拉到400,构建时间多花一点没关系,查询时efSearch可以适当加大到256以上,有时候效果比换距离算法明显。还有个容易忽略的点是query处理,你测试集里的问题本身是不是也做同样的切块或清洗?如果query太长或者带无关前缀,向量会被稀释掉,建议对query也做轻量级改写或提取关键句。最后可以看看是不是Milvus的segment归档策略导致删改后索引碎片化,做一次compact或者重建索引,有时候召回率低纯粹是数据分布偏移了。
切块策略确实值得优先动刀,固定512字符对中文长文档太粗暴了,语义断点比调索引参数影响大得多。我之前试过用spacy或者simpletextract按句子和段落边界切,召回能涨5个点。另外你提到单独检索看着相关但Recall不高,建议看下测试集标注本身是不是基于完整段落做的,如果标注粒度比检索单元粗,那召回率天花板就定死了。索引参数M和efConstruction在数据量到200万时影响其实有限,不如先试试query侧做hybrid检索,比如把原文句子也过一遍BM25,和向量分数做个RRF融合,往往能救回来不少漏检的case。
切块策略确实很可疑,固定512字符太容易切断语义了,我之前做法律文档检索就吃过这亏,改成按段落和语义边界切,配合少量重叠之后recall直接涨了七八个点。另外你可以看看query和doc的embedding是不是同一套模型生成的,如果有用别的模型做query编码,分布不一致也会拖召回。还有个小点,HNSW的M值如果太小,图连通性不够,召回上限就锁死了,试试32以上,efConstruction也拉起
遇到类似情况,切块策略确实很值得优先怀疑,固定512字符容易把语义边界切断,可以试试按段落或语义完整度来切,或者用重叠窗口减少信息丢失。另外召回率低但相似度看着相关,可能是正例标注本身有偏差,建议抽几十条bad case看看是检索排序问题还是标注问题。索引参数M和efConstruction对召回影响其实没那么大,不如先查一下数据分布,是不是某些高频片段占了太多相似向量。还有个小方向,bge-base对中文长文本可能不是最优,可以试试用bm25或混合检索做初筛再向量精排,成本低效果常有提升。
切块策略这个方向我觉得你猜得挺准的,固定512字符确实太粗暴了,中文长文档里一个语义完整的段落经常被拦腰截断,向量里混进半个不相关的句子,召回自然就拉了。我之前遇到过类似情况,后来按章节标题和段落边界做递归切分,再把相邻块做个15%的重叠,Recall直接涨了七八个点,你可以先试试这个。另外你提到HNSW的M和efConstruction,这俩参数其实对召回率影响没那么直接,它们更多是影响索引质量和查询速度的平衡,反而如果构建索引时数据没打乱,或者用了默认的build参数,高维向量容易形成局部“坏桶”,不如把索引删了重新用更保守的M(比如32)和efConstruction(比如400)建一遍,有时候比调nprobe管用。还有个容易忽略的点是query和doc的embedding是不是走的同一个模型和同一个预处理流程,比如你有没有对查询做和切块一致的长度截断?如果query是短文本,doc是长文本,bge在短文本上的表征分布可能和长文本有偏差,这时候可以试试给query做个简单的伪文档扩展,比如把检索到的top结果拼回去再embed一次。最后,如果测试集里的正例本身是那种“跨段落语义”的题目,那Recall@10到65%可能已经接近这个切块粒度下的天花板了,得考虑是不是该上rerank或者用LLM做段落级合并,而不是死磕向量召回。
说实话你切块512字符这个点,我第一反应也是这里。中文长文档语义边界很吃切块策略,固定长度硬切大概率会把完整逻辑打断,导致向量表征时丢失上下文,召回自然就虚了。建议试试基于语义段落或者滑动窗口重叠切块,哪怕先按句号分句再合并到接近512也行,这个改动往往比调索引参数见效快。
另外你提到embedding模型不够强,但bge-base在中文上其实够用,除非你的领域术语特别重。我更怀疑的是query和文档的表示不对齐——比如你测试集里的问句是不是口语化程度高,而文档是书面语?如果是,可以试试对query做一下改写或者加个前缀模板,很多项目靠这个就能拉回几个点。
索引这边,HNSW的M和efConstruction确实值得看,但200万条384维也不算特别大,你Recall@10到65%更像是检索环节和重排环节脱节。Milvus里可以试试开个粗排,用更精确的距离计算在召回结果里再过滤一遍,或者直接上RRF融合多个检索路径的结果,比如同时用向量和关键词BM25,效果往往比单靠向量稳。
最后问一下,你测试集里的正例是怎么定义的?是人工标注的“相关段落”,还是基于某个已有答案反推的?如果是后者,可能标注本身就有噪声,65%的Recall未必全是系统的问题。这行有时候不是调参能救的,得先确认你评估的基准是否真的合理。
说实话你这个问题我太有同感了,之前也卡在召回率上死活上不去。切块策略确实值得优先动刀,我试过按语义段落切加上少量重叠,效果比固定字符数好不少,尤其对长文档里的完整概念特别明显。另外你提到HNSW的M和efConstruction,这俩其实影响挺大的,M调大点能让图更稠密,召回率会有提升,但内存和构建时间也得权衡。还有个小坑,你换IP距离的话,得确认向量归一化没,不然方向对了数值没对齐,效果可能白调。你现在这个Recall@10,是只看top10里有没有正确答案,还是有考虑答案跨多个块的情况?这个定义不同,排查思路也会差挺多的。