• 欢迎关注我的微信公众号“ Falost ” 右边扫描关注 --->>

多模态 RAG 实测(六):页面检索拆成四个字段——正文列单独打分 R@5 只有 0.344,拆开字段后 0.689

AI / 大模型 神棍 103℃ 0评论

上篇我把检索从页下沉到表,把表级这一段做完了:Gold 表所在页只要进了页面 top-10,表级检索 65/65 全中;而 Gold 页进 top-10 只有 65/114(0.5702)。结论很直接——瓶颈全在页面,继续调表级参数是浪费。

这篇按上篇预告来收拾页面检索:正文 + 标题 + 表头 + URL 路径四个字段分别算分再加权。但我没敢直接上那 247 页真实语料。原因是先在工具链上踩了四个坑,不把它们搞清楚,跑出来的数都是假的。这篇先把机制钉死,再给一组可复现的对照实验,真实语料的回归放到下篇。

一、四个坑,都在 SQLite FTS5 上

坑 1:unicode61 会把数值切碎,还会把中文粘成一整块

我用 fts5vocab 把 FTS5 实际存的 token 打出来看,结果和直觉差很远:

1.00        -> 1 , 00
1,234.56    -> 1 , 234 , 56
13.7%       -> 13 , 7
Recall@5    -> recall , 5
/p/844.html -> p , 844 , html
第2行第7列   -> 第2行第7列     (一个 token)

两件事直接影响后面的设计。第一,”1.00″ 不是一个词,是两个 token,”1,234.56″ 是三个——所以按数值查表时,我实际查的是 “1” 和 “00” 这种 idf 极低的碎片。第二更麻烦:按官方文档,unicode61 把”字母和数字”类别的字符都当 token 字符,其余当分隔符;中文汉字属于字母类,中文又不写空格,于是一整段连续中文会被吞成一个 token。我上篇那 12 道坐标题在页面检索里不是”信号弱”,是中文压根没被分词。

坑 2:内置 bm25 的 k1 和 b 是写死的

FTS5 的 bm25() 里 k1=1.2、b=0.75。官方文档写明这是硬编码常量,没法通过参数改。我前几轮一直用 k1=1.5、b=0.75,换成 FTS5 内置之后口径就变了——要么接受 1.2,要么自己实现打分。

坑 3:内置 IDF 和”教科书 IDF”不是一个东西

这个坑是反推出来的。我构造了 N=247 篇、词频可控的语料,逐个量 FTS5 隐含的 IDF,得到:

FTS5 内置:IDF = max( ln((N - n + 0.5) / (n + 0.5)), 1e-6 )
经典 BM25:IDF = ln( 1 + (N - n + 0.5) / (n + 0.5) )

差别有两点:括号里没有那个 “+1″,以及负值被压到 1e-6。后果很具体:

词 df / N FTS5 内置 IDF 经典 IDF
“1” 148 / 247(60%) 0.000001 0.5128
“1” 90 / 247(36%) 0.5541 1.0081

一个词只要出现在一半以上的文档里,FTS5 给它的 IDF 就是 1e-6,等于把这个词从打分里删掉。表格语料里这太常见了。上篇”值→列名”那类题几乎没有区分度,这里能对上号。

坑 4:权重传 0,不等于”不看这一列”

bm25(ft, 1.0, 0.0, 0.0, 0.0) 并不会把匹配限制在第一列。传 0 只是让那一列的词频贡献为 0,文档照样进结果集,只是拿一个 0 分。真要按字段限制,得用列过滤语法:

SELECT rowid FROM ft
 WHERE ft MATCH '{headers} : ("drawdown")'
 ORDER BY bm25(ft, 1.0, 1.0, 1.0, 1.0);

我前面几轮的”纯正文 BM25″如果写成权重 0,其实是错的。

二、对照实验怎么搭的

机制清楚了,先拿一套可复现的合成语料跑对照:247 篇页文档(和真实语料同量级),每篇四个字段 body / title / headers / urlpath。内容词表 953 个、平均每词约出现 3 次,正文里混 3~6 个来自其他页的噪声词;文档长度 28~40 token,avgdl=34.49。

90 道题分三类各 30 道:内容题(查询词只出现在 Gold 页表头)、值→列题(查询词是裸数值)、多词内容题(两个词都来自 Gold 页正文)。指标 R@1 / R@5 / R@10 / MRR。打分用我自己的 BM25 实现,另开一路跟 FTS5 内置 bm25 对照。

三、跑出来的结果

系统 R@1 R@5 R@10 MRR
A 正文列单独打分(列过滤 {body}) 0.3444 0.3444 0.3556 0.3472
F 四字段直接拼成一个字段 0.4111 0.6444 0.6889 0.4988
F2 拼成一个字段 + 经典 IDF 0.4111 0.6556 0.7000 0.5063
D 字段加权 (1,2,2,1) + 经典 IDF 0.4556 0.6889 0.7000 0.5630
B 字段加权 (1,3,3,1),FTS5 内置 0.4556 0.6778 0.6889 0.5555
E RRF 四路融合 0.3444 0.6556 0.6889 0.4521

权重扫描(经典 IDF,权重同时作用在 title 和 headers 上,body 和 urlpath 固定为 1):

(body, title, headers, urlpath) R@1 R@5 MRR
(1,1,1,1) 0.4111 0.6556 0.5063
(1,2,2,1) 0.4556 0.6889 0.5630
(1,3,3,1) 0.4556 0.6889 0.5630
(1,5,5,1) 0.4222 0.6889 0.5445
(1,8,8,1) 0.3000 0.6889 0.4782
(1,12,12,1) 0.2111 0.6889 0.4156

按题型拆 R@5:

题型 n A 正文列 F 拼接 D 加权 B 加权(FTS5) E RRF
内容题(表头词) 30 0.0000 0.9000 1.0000 1.0000 0.9333
值→列题(数值) 30 0.0333 0.0667 0.0667 0.0333 0.0333
多词内容题(正文词) 30 1.0000 1.0000 1.0000 1.0000 1.0000

四、五条结论

1. 正文列单独打分是最差的,R@5 只有 0.3444,内容题直接 0。原因不玄:这批题的问法来自表头,词根本不在正文里。上篇”表头才是那张表的身份证”这句话,在页面级同样成立。

2. 四个字段拼在一起就能从 0.3444 抬到 0.6556,而且不用写任何额外代码——建索引时把 title / headers / urlpath 拼到 body 后面就行。这是性价比最高的一步,先做它。

3. 分字段加权比拼接再好一点,但权重不是越大越好。甜点区在 2~3:(1,2,2,1) 拿到 R@5 0.6889、MRR 0.5630(对拼接的 0.5063)。权重抬到 8 和 12,R@5 卡死在 0.6889 不动,R@1 从 0.4556 掉到 0.3000 / 0.2111,MRR 掉到 0.4782 / 0.4156。权重过大等于让标题和表头里的噪声词也能一票定生死。

4. RRF 这次没赢。四路等权融合 R@5 0.6556、MRR 0.4521,比拼接还差。原因不复杂:四路里只有标题和表头两路有信号,正文和 URL 基本是噪声,等权融合把好信号的排名拉平了。

5. 值→列题依然是死的。所有系统都在 0.03~0.07,和上篇一致。这也印证了坑 1 和坑 3:查询词是 “1.00” 这种被切碎的数值碎片,df 极高,IDF 趋零,排序约等于随机。检索层修不了,只能走索引直答。

五、顺手验了一件事:权重只放大词频,不改长度归一化

我一直不确定 FTS5 的权重是只作用在词频上,还是也作用在文档长度上。做法是照官方公式手写一遍打分,跟 FTS5 的输出逐位比对:

score = -IDF * (W * (k1 + 1)) / (W + k1 * (1 - b + b * |D| / avgdl))
其中 W = Σ w_c * f_c

拿 w=(2,5,3,1) 这组权重试,6 篇文档全部逐位对齐(浮点相等)。结论:权重要乘进词频,但 |D| 和 avgdl 用的是整行不加权的总 token 数。所以想靠加权去改长度惩罚是做不到的,它只调”哪个字段的词更重要”。另外 |D| 用的是 FTS5 分词后的 token 数:”/p/844″ 我按空格切是 1 个,FTS5 看成 3 个,手写实现和 FTS5 对不上通常就出在这儿。

总结

一句话:页面检索拆字段是对的,但顺序不能反——先把四个字段拼在一起(R@5 0.3444 → 0.6556),再加分字段加权(→ 0.6889)。权重给 2~3 就够,给到 8 以上 R@1 和 MRR 一起掉。

三条落地:

  1. 建 FTS5 索引时把 title / headers / urlpath 显式建成独立列,别只索引正文。表头词不在正文里,这是上篇和这篇验证过的同一个坑。
  2. 别用权重 0 来”只看某一列”,用 {col} : ... 列过滤,两者行为不一样。
  3. 用内置 bm25 之前先量一遍 IDF。df 超过语料一半的词,FTS5 直接给你 1e-6。数值列尤其要小心——”1.00″ 会被切成 “1” 和 “00” 两个高频碎片。

下期预告

这篇是合成语料上的机制验证,下一步把它挪到真实那 247 页上跑回归:同样的四字段索引、同样的 (1,2,2,1) 权重,看 Gold 页进 top-10 能不能从 0.5702 抬上去。同时把那 16 道拿错表的题逐条复盘——12 道坐标题加 4 道”值→列名”,看分字段召回能不能把后者救回来。还有一个排上日程的改动:这轮确认了 unicode61 对中文完全不分词,坐标题那类中文问法得先解决分词,我准备切到 trigram 分词器再跑一轮对照。

参考文献

SQLite FTS5 官方文档(bm25() 函数、k1/b 常量、列权重、列过滤语法):
https://www.sqlite.org/fts5.html

SQLite FTS5:fts5vocab 虚拟表(本文打印实际 token 用的就是它):
https://www.sqlite.org/fts5.html#the_fts5vocab_virtual_table

SQLite FTS5:unicode61 分词器(字母/数字为 token 字符、其余为分隔符的规则):
https://www.sqlite.org/fts5.html#unicode61_tokenizer

SQLite FTS5:trigram 分词器(子串匹配,中文场景的备选):
https://www.sqlite.org/fts5.html#the_trigram_tokenizer

Robertson, Zaragoza:The Probabilistic Relevance Framework: BM25 and Beyond(BM25 与字段加权的理论基础):
https://www.staff.city.ac.uk/~sbrp622/papers/foundations_bm25_review.pdf

Apache Lucene:BM25Similarity 源码(对照用:Lucene 的 IDF 是带 +1 的那一版):
https://github.com/apache/lucene/blob/main/lucene/core/src/java/org/apache/lucene/search/similarities/BM25Similarity.java

Cormack, Clarke, Buettcher:Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods(RRF 出处,K=60):
https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf

上一篇:多模态 RAG 实测(五):把检索从页下沉到表(Gold 页 top-10 = 0.5702、表级 65/65 的出处):
https://www.fedte.cc/p/844.html

上上篇:多模态 RAG 实测(四):把”数格子”从模型手里拿走:
https://www.fedte.cc/p/843.html

转载请注明:Falost的小窝 » 多模态 RAG 实测(六):页面检索拆成四个字段——正文列单独打分 R@5 只有 0.344,拆开字段后 0.689

如果你觉得这篇文章不错或者对你有帮助,想请我喝一杯咖啡,可以打赏
喜欢 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址