Files
QMDSearch/README.md
T
kplam fdb664e546 refactor: 统一代码格式,调整多行代码换行风格
对多个文件进行代码格式化调整,将长行参数拆分为多行书写,提升代码可读性,包括:
- 调整函数定义、调用的多行换行格式
- 优化列表、元组、字典的多行排版
- 新增README.md项目说明文档
2026-07-30 14:25:12 +08:00

309 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QMDSearch
面向 AI Agent 的分层信息检索服务,支持多层级知识库检索、向量语义搜索和结构化数据查询。基于 Docker 部署在 NAS 上,为 AI Agent 提供高效、精准的信息检索能力。
## 特性
- **分层检索架构**:L1 文档定位 → L2 章节大纲 → L3 小节定位 → chunk 原文,逐层收窄,精准召回
- **Hybrid 检索**dense 向量 + sparse 稀疏向量 RRF 融合,兼顾语义与关键词匹配
- **文档三级总结**:Ollama 本地模型自动生成文档 L1/L2/L3 总结,支持知识分类
- **多格式文件入库**:支持 `.txt` `.md` `.html` `.htm` `.pdf` `.docx`PDF 扫描件自动 OCR 降级
- **文本去重**:Sha256 文本去重,重复文档自动复用已有结果
- **JWT 认证**:Token 鉴权,支持注册开关与默认管理员
- **管理后台**`/admin` 单页面,含概览、文档管理、检索测试台、类目管理
- **Docker 部署**:开箱即用的 Docker Compose 编排,适合 NAS 环境
## 技术栈
| 组件 | 技术 |
|------|------|
| 语言 | Python 3.12+ |
| Web 框架 | FastAPI + Uvicorn |
| 向量数据库 | QdrantDocker |
| 缓存 | RedisDocker |
| 嵌入模型 | OpenAI / Ollama 本地模型(可切换) |
| 本地推理 | Ollama + qwen2.5:1.5b(文档总结) |
| OCR | rapidocr-onnxruntime + pypdfium2(扫描件 PDF |
| 部署 | Docker Compose on NAS |
## 项目结构
```
QMDSearch/
├── app/ # 应用主目录
│ ├── main.py # FastAPI 入口(含 /admin 管理页面)
│ ├── config.py # 配置管理(pydantic-settings / 环境变量)
│ ├── api/ # API 路由层
│ │ ├── response.py # 统一响应格式
│ │ └── v1/ # API v1
│ │ ├── search.py # 检索接口
│ │ ├── document.py # 文档入库/管理/上传
│ │ ├── knowledge.py # 知识库接口
│ │ └── auth.py # 认证接口
│ ├── core/ # 核心业务逻辑
│ │ ├── retriever.py # 分层检索引擎
│ │ ├── query_parser.py # Query 解析与分类路由
│ │ ├── embeddings.py # 向量嵌入
│ │ ├── sparse.py # 稀疏向量编码
│ │ ├── ranker.py # RRF 融合与结果截断
│ │ ├── summarizer.py # 文档三级总结
│ │ ├── headings.py # 原生标题树解析
│ │ ├── classifier.py # 文档分类
│ │ ├── chunker.py # 标题树感知 chunk 切分
│ │ ├── ingestion.py # 文档入库流水线
│ │ ├── ingest_tasks.py # 异步任务管理(含文本去重)
│ │ ├── file_parser.py # 多格式文件解析(含 PDF OCR)
│ │ └── auth.py # JWT 认证
│ ├── models/ # Pydantic 数据模型
│ ├── services/ # 外部服务客户端
│ │ ├── qdrant.py # Qdrant 客户端
│ │ ├── redis.py # Redis 缓存客户端
│ │ └── ollama.py # Ollama 客户端
│ └── static/ # 静态资源
│ └── admin.html # 管理后台(单文件)
├── tests/ # 测试
├── scripts/ # 运维与评测脚本
│ ├── eval/ # 回归评测
│ └── smoke_live.py # 在线冒烟
├── docker-compose.yml # Docker 编排
├── Dockerfile # 应用镜像
├── .env.example # 环境变量模板
└── pyproject.toml # 项目配置
```
## 分层检索架构
```
Query → 解析路由 → L1(文档总结,候选文档集)
L2(章节大纲,按 section 剪枝)
L3(小节定位,两路 RRF 融合)
Chunk(原文 hybrid 检索,dense + sparse
Top-K 结果
```
### 检索链路详解
1. **Query 解析路由**Ollama 小模型将 query 解析为结构化结果(rewrite、关键词、命中类目+置信度)。高置信且类目数不超上限时按类目过滤,否则全库兜底。
2. **L1 - 文档定位层**:在文档 L1 总结集合中检索,产出候选文档集合。无命中时直接全库 chunk 兜底。
3. **L2 - 章节大纲层**:在候选文档内检索 L2 章节大纲,按 section 剪枝定位。未命中的文档落入 L3 b 路。
4. **L3 - 小节内容层**:两路查询(aL2 命中文档按 section_path 过滤;b:其余文档仅按 doc_id 过滤)后 RRF 融合。
5. **Chunk 层**:在 L3 收窄的范围内对原文 chunk 做 hybrid 检索(dense 向量 + sparse 稀疏向量 RRF 融合),返回最终 Top-K。
## 快速开始
### 前置要求
- Docker & Docker Compose
- OpenAI API Key(或本地 Ollama 模型)
### 1. 克隆项目
```bash
git clone <repo-url>
cd QMDSearch
```
### 2. 配置环境变量
```bash
cp .env.example .env
# 编辑 .env,至少配置 OPENAI_API_KEY
```
### 3. 启动服务
```bash
docker compose up -d
```
### 4. 拉取 Ollama 模型(首次启动后)
```bash
docker exec qmdsearch-ollama ollama pull qwen2.5:1.5b
```
### 5. 验证服务
```bash
curl http://localhost:8000/api/v1/health
# {"status": "ok"}
```
管理后台:浏览器访问 `http://localhost:8000/admin`
## API 文档
| 方法 | 路径 | 说明 | 认证 |
|------|------|------|------|
| `GET` | `/api/v1/health` | 健康检查 | 否 |
| `POST` | `/api/v1/auth/register` | 用户注册 | 否(可关闭) |
| `POST` | `/api/v1/auth/login` | 用户登录,返回 JWT token | 否 |
| `GET` | `/api/v1/auth/me` | 获取当前用户信息 | 是 |
| `POST` | `/api/v1/search` | 分层检索 | 是 |
| `POST` | `/api/v1/documents` | 文档入库(JSON 文本,202 异步入库) | 是 |
| `POST` | `/api/v1/documents/upload` | 文件上传入库(multipart202 异步) | 是 |
| `GET` | `/api/v1/documents/tasks/{task_id}` | 入库任务状态查询 | 是 |
| `GET` | `/api/v1/documents` | 文档列表(分页) | 是 |
| `GET` | `/api/v1/documents/{doc_id}` | 文档详情 | 是 |
| `DELETE` | `/api/v1/documents/{doc_id}` | 删除文档(幂等) | 是 |
| `GET` | `/api/v1/knowledge/categories` | 知识分类类目集 | 是 |
| `GET` | `/api/v1/knowledge/stats` | 统计(四层点数 + 类目分布) | 是 |
| `GET` | `/admin` | 管理后台 | 否 |
### 统一响应格式
```json
{
"code": 0,
"data": { ... },
"message": "ok"
}
```
错误码:`0` 成功,`1xxx` 客户端错误,`2xxx` 服务端错误。
### 检索示例
```bash
curl -X POST http://localhost:8000/api/v1/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"query": "如何配置 Redis 缓存", "top_k": 5}'
```
### 文件上传示例
```bash
curl -X POST http://localhost:8000/api/v1/documents/upload \
-H "Authorization: Bearer <token>" \
-F "file=@document.pdf"
```
## 文档入库流程
```
文件上传 → 文本提取 → 三级总结(Ollama)→ 分类判定(L1 总结)→ 向量化 → 写入 Qdrant
不足三级 → 2.5 级回退
```
### 三级总结
| 层级 | 名称 | 说明 |
|------|------|------|
| L1 | 总结 | 对整篇文档的一句话高度概括 |
| L2 | 大纲 | 文档主要章节和关键主题 |
| L3 | 内容大纲 | 每个章节的详细内容摘要 |
**2.5 级回退**:短文本(< 500 字符)自动降级为二级总结,跳过大纲层。
### 文件格式支持
| 格式 | 解析方式 |
|------|---------|
| `.txt` `.md` | UTF-8 解码 |
| `.html` `.htm` | HTML 标签剥离 |
| `.pdf` | pypdf 提取文本层;扫描件自动 OCR 降级 |
| `.docx` | python-docx 段落提取 |
### PDF OCR 降级
图片型/扫描件 PDF 在文本层为空时,自动触发 OCR:
1. pypdfium2 渲染每页为图片
2. rapidocr-onnxruntime 识别文字
3. 拼接返回结果
可通过环境变量 `PDF_OCR_ENABLED=false` 关闭,或通过 `PDF_OCR_MAX_PAGES` / `PDF_OCR_DPI` 控制行为。
### 文本去重
入库时自动计算 `sha256(doc.text)`,相同文本重复提交不重跑流水线,直接复用已有文档 ID,结果中 `deduplicated=true` 标记。
## 配置项
所有配置通过环境变量注入,完整列表见 `.env.example`。主要配置项:
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `EMBEDDING_PROVIDER` | 嵌入模型提供商(openai / local | `openai` |
| `OPENAI_API_KEY` | OpenAI API Key | - |
| `EMBEDDING_MODEL` | 嵌入模型名称 | `text-embedding-3-small` |
| `OLLAMA_MODEL` | Ollama 本地模型 | `qwen2.5:1.5b` |
| `RETRIEVAL_TOP_K` | 检索召回数 | `20` |
| `RETRIEVAL_FINAL_K` | 最终返回数 | `5` |
| `INGEST_MAX_CONCURRENCY` | 入库并发上限 | `2` |
| `JWT_SECRET_KEY` | JWT 签名密钥(生产必填) | 自动生成(仅开发) |
| `AUTH_REGISTER_ENABLED` | 是否开放注册 | `true` |
| `UPLOAD_MAX_SIZE_MB` | 上传文件大小上限 | `20` |
| `PDF_OCR_ENABLED` | 是否启用 PDF OCR | `true` |
| `PDF_OCR_MAX_PAGES` | OCR 最大页数 | `30` |
| `PDF_OCR_DPI` | OCR 渲染 DPI | `200` |
## 开发
### 本地开发
```bash
# 安装依赖
uv sync --extra dev
# 启动开发服务器
uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# 运行测试
uv run pytest
# 代码检查
uv run ruff check app/ tests/
```
### 构建镜像
```bash
docker compose build
```
### 目录结构约定
- 类型注解必须(Python 3.12+ 语法)
- 异步优先(async/await
- 配置通过环境变量注入(pydantic-settings
- 日志使用 structlog 结构化日志
- 测试使用 pytest + pytest-asyncio
## 部署
### NAS 部署
```bash
# 1. 创建数据目录
mkdir -p /path/to/nas/data/{qdrant,redis,ollama}
# 2. 配置 .env,设置 NAS_DATA_DIR 和 API Key
NAS_DATA_DIR=/path/to/nas/data
OPENAI_API_KEY=sk-xxx
# 3. 启动
docker compose up -d
# 4. 查看日志
docker compose logs -f app
```
### 端口映射
| 服务 | 端口 |
|------|------|
| API | 8000 |
| Qdrant | 6333 |
| Qdrant Dashboard | 6334 |
| Redis | 6379 |
| Ollama | 11434 |
## License
MIT