kplam f92eff6f65 feat: add document file download API and related features
1. add `/api/v1/documents/{doc_id}/file` endpoint for downloading original document files
2. add file field to document detail API response based on metadata
3. add comprehensive tests for file download API and metadata integration
4. update API documentation in CLAUDE.md
2026-07-31 22:46:50 +08:00

QMDSearch

面向 AI Agent 的分层信息检索服务,支持多层级知识库检索、向量语义搜索和结构化数据查询。基于 Docker 部署在 NAS 上,为 AI Agent 提供高效、精准的信息检索能力。

特性

  • 分层检索架构:L1 文档定位 → L2 章节大纲 → L3 小节定位 → chunk 原文,逐层收窄,精准召回
  • Hybrid 检索dense 向量 + sparse 稀疏向量 RRF 融合,兼顾语义与关键词匹配
  • 文档三级总结:Ollama 本地模型自动生成文档 L1/L2/L3 总结,支持知识分类
  • 多格式文件入库:支持 .txt .md .html .htm .pdf .docxPDF 扫描件自动 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. 克隆项目

git clone <repo-url>
cd QMDSearch

2. 配置环境变量

cp .env.example .env
# 编辑 .env,至少配置 OPENAI_API_KEY

3. 启动服务

docker compose up -d

4. 拉取 Ollama 模型(首次启动后)

docker exec qmdsearch-ollama ollama pull qwen2.5:1.5b

5. 验证服务

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 管理后台

统一响应格式

{
  "code": 0,
  "data": { ... },
  "message": "ok"
}

错误码:0 成功,1xxx 客户端错误,2xxx 服务端错误。

检索示例

curl -X POST http://localhost:8000/api/v1/search \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{"query": "如何配置 Redis 缓存", "top_k": 5}'

文件上传示例

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

开发

本地开发

# 安装依赖
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/

构建镜像

docker compose build

目录结构约定

  • 类型注解必须(Python 3.12+ 语法)
  • 异步优先(async/await
  • 配置通过环境变量注入(pydantic-settings
  • 日志使用 structlog 结构化日志
  • 测试使用 pytest + pytest-asyncio

部署

NAS 部署

# 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

S
Description
AI Agent 分层信息检索服务
Readme 927 KiB
Languages
Python 77.2%
HTML 10.6%
Vue 9.6%
JavaScript 2.3%
CSS 0.2%
Other 0.1%