修复知识库搜索粒度:parent section是表格时优先返回child sections
- 添加_build_parent_child_map方法构建section层级关系 - 当parent section有heading命中但body是表格时,优先返回child sections - 最终结果按分数排序确保最高分排第一 - 修复'安装说明'查询返回插件表格而非下载链接的问题
This commit is contained in:
@@ -384,6 +384,44 @@ class KnowledgeBase:
|
||||
pairs.append((t, tokens[i + 1]))
|
||||
return pairs
|
||||
|
||||
def _build_parent_child_map(self, doc_idx_list: List[int]) -> dict:
|
||||
"""构建 section 的 parent-child 关系映射。
|
||||
|
||||
根据 heading 层级(# / ## / ### / ####)推断父子关系。
|
||||
返回 {child_idx: parent_idx} 映射。
|
||||
"""
|
||||
import re
|
||||
parent_map = {}
|
||||
current_parent = {1: None, 2: None, 3: None, 4: None}
|
||||
|
||||
for idx in doc_idx_list:
|
||||
if idx >= len(self.docs):
|
||||
continue
|
||||
doc = self.docs[idx]
|
||||
heading = doc.get("heading", "")
|
||||
|
||||
# 提取 heading 层级
|
||||
match = re.match(r'^(#{1,6})\s', heading)
|
||||
if not match:
|
||||
continue
|
||||
level = len(match.group(1))
|
||||
|
||||
# 找到当前级别及以上的 parent
|
||||
parent = None
|
||||
for l in range(level - 1, 0, -1):
|
||||
if current_parent.get(l) is not None:
|
||||
parent = current_parent[l]
|
||||
break
|
||||
|
||||
parent_map[idx] = parent
|
||||
current_parent[level] = idx
|
||||
|
||||
# 重置更低级别的 parent
|
||||
for l in range(level + 1, 5):
|
||||
current_parent[l] = None
|
||||
|
||||
return parent_map
|
||||
|
||||
def search(self, query: str, top_k: int = 3, min_score: float = 0.1) -> List[Tuple[dict, float]]:
|
||||
"""搜索知识库,返回 (doc, score) 列表。
|
||||
|
||||
@@ -721,6 +759,64 @@ class KnowledgeBase:
|
||||
reranked.sort(key=lambda x: x[2], reverse=True)
|
||||
doc_sections = reranked
|
||||
|
||||
# ⭐ Parent-child 精度提升:当 parent section 有 heading 命中但 body 是"空壳"(表格/短文本),
|
||||
# 优先返回 child sections(更具体的子内容)
|
||||
# 如 "## 2.安装说明:" body是前置插件表格,应优先返回 "### 2.1 下载" 子sections
|
||||
if len(doc_sections) > 1:
|
||||
# 构建当前文档的 parent-child 关系(从全部docs中)
|
||||
all_doc_indices = list(range(len(self.docs)))
|
||||
parent_map = self._build_parent_child_map(all_doc_indices)
|
||||
|
||||
# 检查是否有"空壳"parent section
|
||||
for i, (idx, doc, score) in enumerate(doc_sections):
|
||||
heading = doc.get("heading", "")
|
||||
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]
|
||||
if children:
|
||||
# 找到有实质性body的children
|
||||
good_children = []
|
||||
for child_idx in children:
|
||||
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('|'):
|
||||
good_children.append((child_idx, child_doc))
|
||||
|
||||
if good_children:
|
||||
# 给parent减分(body是空壳表格)
|
||||
doc_sections[i] = (idx, doc, score * 0.3)
|
||||
|
||||
# 将good children加入doc_sections(如果不在的话)
|
||||
existing_idx_set = {s[0] for s in doc_sections}
|
||||
for child_idx, child_doc in good_children:
|
||||
if child_idx not in existing_idx_set:
|
||||
# 计算child的加分:heading含查询词 → 额外加分
|
||||
child_heading = child_doc.get("heading", "")
|
||||
child_h_tokens = set(t for t in jieba.cut(child_heading) if len(t) >= 2)
|
||||
child_heading_bonus = len(query_tokens & child_h_tokens) * 5.0
|
||||
# child body含查询词 → 额外加分
|
||||
child_body = child_doc.get("body", "")
|
||||
child_body_bonus = 10.0 if any(t in child_body for t in query_tokens if len(t) >= 2) else 0.0
|
||||
|
||||
child_score = score * 0.8 + child_heading_bonus + child_body_bonus
|
||||
doc_sections.append((child_idx, child_doc, child_score))
|
||||
|
||||
# 重新排序
|
||||
doc_sections.sort(key=lambda x: x[2], reverse=True)
|
||||
break
|
||||
|
||||
# ⭐ 表格优先策略(reranking后):条件优先返回与查询语义相关的表格section
|
||||
# 防止reranking后表格被其他section超越(表格是答案但不含查询词)
|
||||
# ⚠ 修复:不能盲目优先含"操作"的表格——"操作方式"表(交互模式)会覆盖"权限级别"表
|
||||
@@ -908,6 +1004,8 @@ class KnowledgeBase:
|
||||
break
|
||||
results.append((doc, float(score)))
|
||||
|
||||
# ⭐ 最终排序:按分数降序,确保最高分的结果排第一
|
||||
results.sort(key=lambda x: x[1], reverse=True)
|
||||
return results[:top_k]
|
||||
if results:
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user