Initial commit: QMDSearch 分层信息检索服务
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""标题树解析器的单元测试"""
|
||||
|
||||
from app.core.headings import parse_headings, render_outline
|
||||
|
||||
|
||||
class TestMarkdownHeadings:
|
||||
"""Markdown ATX 标题解析"""
|
||||
|
||||
def test_multi_level(self):
|
||||
"""多级 Markdown 标题,层级为 # 数量"""
|
||||
text = "# 一级标题\n正文内容\n## 二级标题\n更多内容\n### 三级标题\n"
|
||||
headings = parse_headings(text)
|
||||
assert [(h.title, h.level, h.line_index) for h in headings] == [
|
||||
("一级标题", 1, 0),
|
||||
("二级标题", 2, 2),
|
||||
("三级标题", 3, 4),
|
||||
]
|
||||
|
||||
def test_hash_without_space_not_heading(self):
|
||||
"""# 后无空白不是标题"""
|
||||
assert parse_headings("#不是标题") == []
|
||||
|
||||
def test_up_to_six_levels(self):
|
||||
"""最多支持 6 级标题"""
|
||||
text = "###### 六级标题\n####### 七级不算\n"
|
||||
headings = parse_headings(text)
|
||||
assert len(headings) == 1
|
||||
assert headings[0].level == 6
|
||||
|
||||
|
||||
class TestChineseChapterHeadings:
|
||||
"""中文章/节/篇编号标题解析"""
|
||||
|
||||
def test_chapter_and_section(self):
|
||||
"""章=1 级,节=2 级"""
|
||||
text = "第一章 总则\n正文\n第一节 一般规定\n"
|
||||
headings = parse_headings(text)
|
||||
assert [(h.title, h.level) for h in headings] == [("第一章 总则", 1), ("第一节 一般规定", 2)]
|
||||
|
||||
def test_pian_is_level_one(self):
|
||||
"""篇=1 级,支持阿拉伯数字编号"""
|
||||
text = "第一篇 概述\n第2章 背景\n"
|
||||
headings = parse_headings(text)
|
||||
assert [(h.title, h.level) for h in headings] == [("第一篇 概述", 1), ("第2章 背景", 1)]
|
||||
|
||||
def test_chinese_enum_is_level_one(self):
|
||||
"""一、二、…… 固定 1 级"""
|
||||
text = "一、项目背景\n二、建设目标\n"
|
||||
headings = parse_headings(text)
|
||||
assert [(h.title, h.level) for h in headings] == [("一、项目背景", 1), ("二、建设目标", 1)]
|
||||
|
||||
def test_overlong_line_not_heading(self):
|
||||
"""编号行超过 60 字符视为正文"""
|
||||
text = "第一章 " + "很长的标题" * 12 + "\n"
|
||||
assert parse_headings(text) == []
|
||||
|
||||
|
||||
class TestNumericHeadings:
|
||||
"""数字编号标题解析"""
|
||||
|
||||
def test_dot_segments_determine_level(self):
|
||||
"""按点分段数定层级:1.=1、1.1=2、1.1.1=3"""
|
||||
text = "1. 概述\n1.1 背景\n1.1.1 细节\n"
|
||||
headings = parse_headings(text)
|
||||
assert [(h.title, h.level) for h in headings] == [
|
||||
("1. 概述", 1),
|
||||
("1.1 背景", 2),
|
||||
("1.1.1 细节", 3),
|
||||
]
|
||||
|
||||
def test_dunhao_separator(self):
|
||||
"""顿号分隔的数字编号为 1 级"""
|
||||
headings = parse_headings("1、基本要求\n2、总体架构\n")
|
||||
assert [(h.title, h.level) for h in headings] == [("1、基本要求", 1), ("2、总体架构", 1)]
|
||||
|
||||
|
||||
class TestMixedAndPlain:
|
||||
"""混合模式与无结构文本"""
|
||||
|
||||
def test_no_headings_returns_empty(self):
|
||||
"""无标题文本返回空列表"""
|
||||
text = "第一段正文内容。\n\n第二段正文内容。\n"
|
||||
assert parse_headings(text) == []
|
||||
|
||||
def test_empty_text(self):
|
||||
assert parse_headings("") == []
|
||||
|
||||
def test_mixed_patterns(self):
|
||||
"""Markdown 与中文编号混合时均能识别"""
|
||||
text = "# Markdown 标题\n正文\n第一章 中文标题\n1.1 数字标题\n"
|
||||
headings = parse_headings(text)
|
||||
assert [(h.title, h.level) for h in headings] == [
|
||||
("Markdown 标题", 1),
|
||||
("第一章 中文标题", 1),
|
||||
("1.1 数字标题", 2),
|
||||
]
|
||||
|
||||
|
||||
class TestRenderOutline:
|
||||
"""标题树渲染为大纲文本"""
|
||||
|
||||
def test_indent_by_level(self):
|
||||
headings = parse_headings("# 安装指南\n## 环境准备\n## 安装步骤\n### 验证\n")
|
||||
assert render_outline(headings) == "- 安装指南\n - 环境准备\n - 安装步骤\n - 验证"
|
||||
Reference in New Issue
Block a user