Initial commit: QMDSearch 分层信息检索服务

- FastAPI + Qdrant + Redis + Ollama 技术栈
- L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合)
- 文档三级总结与 2.5 级回退
- query 解析路由与分类
- /admin 管理页面
This commit is contained in:
2026-07-29 21:24:40 +08:00
commit 51dc8dc4f6
83 changed files with 10794 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
"""Summarizer 标题树接入的单元测试"""
from app.core.summarizer import Summarizer
from app.models.document import SummaryLevel
class FakeOllama:
"""记录调用次数的 OllamaClient 替身"""
def __init__(self) -> None:
self.prompts: list[str] = []
async def generate(self, prompt: str) -> str:
self.prompts.append(prompt)
return "LLM 生成结果"
def _structured_text() -> str:
"""构造带标题结构且长度 ≥ 500 的文档(不触发 2.5 回退)"""
paragraph = "这是章节正文内容,包含足够多的信息量。" * 10
return f"# 安装指南\n{paragraph}\n## 环境准备\n{paragraph}\n## 安装步骤\n{paragraph}"
class TestStructuredDocument:
"""结构化文档:L2 直接使用标题树,不调 LLM"""
async def test_l2_from_headings_skips_llm(self):
ollama = FakeOllama()
summarizer = Summarizer(ollama=ollama) # type: ignore[arg-type]
summary = await summarizer.summarize(_structured_text(), title="安装文档")
assert summary.level == SummaryLevel.L3
# L1 + L3 两次调用,跳过了 L2 的 LLM 调用
assert len(ollama.prompts) == 2
# 大纲由标题树渲染,包含标题文本与层级缩进
assert summary.l2_outline is not None
assert "- 安装指南" in summary.l2_outline
assert " - 环境准备" in summary.l2_outline
assert " - 安装步骤" in summary.l2_outline
assert summary.l2_outline != "LLM 生成结果"
# L3 的 prompt 中注入了标题树大纲
assert "安装指南" in ollama.prompts[1]
class TestPlainDocument:
"""无结构文档:L2 回退 LLM 生成"""
async def test_l2_falls_back_to_llm(self):
ollama = FakeOllama()
summarizer = Summarizer(ollama=ollama) # type: ignore[arg-type]
text = "这是一段没有标题结构的正文内容," * 40 # 640 字符,不触发 2.5 回退
summary = await summarizer.summarize(text, title="")
assert summary.level == SummaryLevel.L3
# L1 + L2 + L3 三次调用
assert len(ollama.prompts) == 3
assert summary.l2_outline == "LLM 生成结果"
class TestFallbackUnaffected:
"""2.5 级回退逻辑不受标题树改造影响"""
async def test_short_structured_text_still_fallback(self):
ollama = FakeOllama()
summarizer = Summarizer(ollama=ollama) # type: ignore[arg-type]
text = "# 标题一\n简短内容。\n# 标题二\n简短内容。"
summary = await summarizer.summarize(text, title="")
assert summary.level == SummaryLevel.L2_HALF
assert summary.l2_outline is None
# L1 + L2.5 两次调用
assert len(ollama.prompts) == 2