idea_world_labDEV JOURNAL
2026年6月28日星期日

方案 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 基准线可使用  
最终搜索策略为废弃候选

原文备注的判断接近于“应该推翻”。