方案 A:保持当前 PostgreSQL 全文检索
流程
raw chunkText
-> plainto_tsquery
-> search_tsv @@ query
-> ts_rank_cd
-> top JSONL 返回优点
- 已经实现。
- 只需要 PostgreSQL 即可。
- 基础设施简单。
- 可以直接匹配已有
search_tsv的表。
缺点
- 不是 BM25。
- 对长代码块不友好。
plainto_tsquery生成的条件可能过于严格。- 代码标记噪声较多。
- 不能进行语义搜索。
- 即使 chunk 的划分稍有不同,也可能出现 0 条结果。
基准 chunk 中可能出现的问题
基准 chunk 中一次性包含以下 token。
func
process
delta
velocity
vector2
zero
input
is_action_pressed
move_right
move_left
normalized
animatedsprite2d
play
stop
position
clamp
screen_size所有这些令牌如果没有同时出现在同一行 JSONL 中,搜索结果可能会缺失。
PoC 模拟
假设基准 chunk 直接传入 /api/retrieve。
输入:
chunkText = 整个 _process(delta) 函数体当前方式将此输入转换为 plainto_tsquery('simple', chunkText)。
预期的查询特性:
func & process & delta & var & velocity & vector2 & zero
& input & is_action_pressed & move_right & move_left
& length & normalized & speed & animatedsprite2d
& play & stop & position & clamp & screen_size此时,如果实际相关文档像下面这样被分成两个块,就会出现问题。
docs chunk A:
Input.is_action_pressed("move_right")
Input.is_action_pressed("move_left")
velocity.normalized() * speed
$AnimatedSprite2D.play()
$AnimatedSprite2D.stop()
docs chunk B:
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)如果人们看到时,A和B都有关联。
但是,如果全文查询一次要求太多的标记,可能会像下面这样。
docs chunk A:
animatedsprite2d = 有
is_action_pressed = 有
move_left/move_right = 有
clamp = 无
screen_size = 无
position.clamp = 无
=> 查询未能满足全部条件
docs chunk B:
clamp = 有
screen_size = 有
position = 有
animatedsprite2d = 不存在或较弱
move_left/move_right = 无
is_action_pressed = 无
=> 查询未能满足全部条件可见的失败形式:
搜索结果 0 条
或者仅返回非常狭窄的块在这种方式下,难以区分搜索失败是“因为文档没有依据而失败”,还是“因为查询条件过严而失败”。
PoC 中需要确认的日志
测试此替代方案时,不能只看结果。
必须一起显示以下值。
1. 原始 chunkText
2. 使用 plainto_tsquery 创建的 query
3. query 中包含的 token 列表
4. 返回的 row 数量
5. 如果为 0 条,哪个 token 导致了过于严格预期观察:
原始块越长,查询就越长;如果相关文档分散在多个块中,结果会缺失。判定
PoC 基准线可使用
最终搜索策略为废弃候选原文备注的判断接近于“应该推翻”。