fix: 知识库搜索返回不相干结果

1. DOC_TOPIC_TOKENS添加php映射,修复'PHP登录模式'查询不匹配php.md
2. parent-child逻辑增加条件2:parent body是表格+任何child heading含查询词
3. heading token计算改用heading_no_links,防止链接文字误匹配
4. 查询含'登录插件'时,protect文档降权从0.5改为0.2
5. 新增php登录模式查询boost:php文档section额外x10加分
6. 新增下载意图优先:查询含'下载'时优先返回heading含下载的section
This commit is contained in:
youpaishidifu
2026-08-08 18:30:02 +08:00
parent bc59f0f5ab
commit cbc4a9266a
+57 -11
View File
@@ -631,6 +631,7 @@ class KnowledgeBase:
# 如"php管理员"应优先返回phpadmin文档,"领地"应优先返回protect文档
# 改为 token 级匹配(而非字面子串),OCR 丢失"php"只剩"管理员端默认"也能命中 phpadmin
DOC_TOPIC_TOKENS = {
'php': {'php', '后台', '玩家端', '登录模式'},
'phpadmin': {'php', 'php管理员', '后端'},
'protect': {'领地', 'protect', '保护'},
'login': {'登录', 'login', '账号', 'sdf1'},
@@ -656,11 +657,22 @@ class KnowledgeBase:
# 防止protect文档因包含"登录插件"链接文本而误排第一
if '登录插件' in query_stripped:
if 'sdf1_login' in doc_best:
doc_best['sdf1_login'] *= 3.0 # 优先返回登录插件文档
doc_best['sdf1_login'] *= 5.0 # 优先返回登录插件文档
if 'login' in doc_best:
doc_best['login'] *= 3.0
doc_best['login'] *= 5.0
if 'protect' in doc_best:
doc_best['protect'] *= 0.5 # 降权protect文档
doc_best['protect'] *= 0.2 # 大幅降权protect文档(含链接文本)
# ⭐ php登录模式查询:查询包含"php"+"登录模式"时,优先返回php文档
# 防止login/sdf1_login文档因含"登录"而压过真正含答案的php文档
if 'php' in query_token_set2 and '登录模式' in query_stripped:
if 'php' in doc_best:
doc_best['php'] *= 10.0 # 巨幅优先php文档
if 'login' in doc_best:
doc_best['login'] *= 0.3 # 大幅降权login文档
if 'sdf1_login' in doc_best:
doc_best['sdf1_login'] *= 0.3 # 大幅降权sdf1_login文档
if 'protect' in doc_best:
doc_best['protect'] *= 0.1 # 大幅降权protect文档(含链接文本)
# ⭐ 管理员 + 默认 组合 → 强偏好 phpadmin 文档
# 此时 protect/全局配置 虽含"管理员+默认",但主题是领地;查询无"领地"时应让位
if q_has_admin and q_has_default:
@@ -786,7 +798,8 @@ class KnowledgeBase:
body = doc.get("body", doc.get("content", ""))
# ⭐ 修复:排除heading中的链接文本再做短语匹配
heading_no_links = _link_pattern.sub('', heading)
h_tokens = set(t for t in jieba.cut(heading) if len(t) >= 2)
# 使用heading_no_links计算token,防止链接文字如"sdf1_login登录插件"误匹配
h_tokens = set(t for t in jieba.cut(heading_no_links) if len(t) >= 2)
# heading 包含查询关键词 → 适度 bonus(2.0 每命中词)
heading_hits = len(query_tokens & h_tokens)
bonus = heading_hits * 2.0
@@ -841,7 +854,7 @@ class KnowledgeBase:
reranked.sort(key=lambda x: x[2], reverse=True)
doc_sections = reranked
# ⭐ Parent-child 精度提升:当 parent section 有 heading 命中但 body 是"空壳"(表格/短文本),
# ⭐ Parent-child 精度提升:当 parent section body 是"空壳"(表格/短文本),
# 优先返回 child sections(更具体的子内容)
# 如 "## 2.安装说明:" body是前置插件表格,应优先返回 "### 2.1 下载" 子sections
if len(doc_sections) > 1:
@@ -855,14 +868,23 @@ class KnowledgeBase:
body = doc.get("body", "")
body_len = len(body.strip())
# 判断是否为"空壳"section:heading含查询词 + body是表格
h_tokens = set(t for t in jieba.cut(heading) if len(t) >= 2)
heading_has_query = bool(query_tokens & h_tokens)
is_table = body.strip().startswith('|') and '|' in body[1:]
if heading_has_query and is_table:
# 找到该section的children(从全部docs中)
children = [c_idx for c_idx, p_idx in parent_map.items() if p_idx == idx]
# 条件1: parent heading含查询词 + body是表格
# 条件2: parent body是表格 + 任何child heading含查询词(即使parent不匹配)
children = [c_idx for c_idx, p_idx in parent_map.items() if p_idx == idx]
child_has_query_heading = False
if children:
for child_idx in children:
child_h = self.docs[child_idx].get("heading", "")
child_h_tokens = set(t for t in jieba.cut(child_h) if len(t) >= 2)
if query_tokens & child_h_tokens:
child_has_query_heading = True
break
if (heading_has_query and is_table) or (is_table and child_has_query_heading):
if children:
# 找到有实质性body的children
good_children = []
@@ -870,7 +892,6 @@ class KnowledgeBase:
child_doc = self.docs[child_idx]
child_body = child_doc.get("body", "")
child_body_len = len(child_body.strip())
child_heading = child_doc.get("heading", "")
# child有非空body且不是表格 → 候选
if child_body_len > 5 and not child_body.strip().startswith('|'):
@@ -917,6 +938,26 @@ class KnowledgeBase:
if heading_relevant or body_relevant:
doc_sections.insert(0, doc_sections.pop(i))
break
# ⭐ 下载意图优先:查询包含"下载"时,优先返回heading含"下载"的section
# 防止"在哪儿能下载到登录插件?"返回login指令而非下载链接
if len(doc_sections) > 1 and '下载' in query_stripped:
for i in range(len(doc_sections)):
idx = doc_sections[i][0]
heading = self.docs[idx].get("heading", "")
_link_pattern = re.compile(r'\[([^\]]*)\]\([^\)]+\)')
heading_no_links = _link_pattern.sub('', heading)
if '下载' in heading_no_links:
doc_sections.insert(0, doc_sections.pop(i))
break
# ⭐ php登录模式意图优先:查询包含"php"+"登录模式"时,优先返回php文档的section
# 防止sdf1_login的login指令压过真正含答案的php后台玩家端section
if len(doc_sections) > 1 and 'php' in query_token_set2 and '登录模式' in query_stripped:
for i in range(len(doc_sections)):
idx = doc_sections[i][0]
doc = self.docs[idx]
if doc.get("title") == "php":
doc_sections.insert(0, doc_sections.pop(i))
break
if has_action and len(doc_sections) > 1:
# 动作查询:提取动词+对象,精准匹配 section
@@ -1084,7 +1125,12 @@ class KnowledgeBase:
for i, (idx, doc, score) in enumerate(doc_sections):
if i >= sub_k:
break
results.append((doc, float(score)))
# ⭐ php登录模式查询:php文档的section额外加分,确保排在sdf1_login之前
final_score = float(score)
if 'php' in query_token_set2 and '登录模式' in query_stripped:
if doc.get("title") == "php":
final_score *= 10.0 # php文档section巨幅加分
results.append((doc, final_score))
# ⭐ 最终排序:按分数降序,确保最高分的结果排第一
results.sort(key=lambda x: x[1], reverse=True)