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
@@ -0,0 +1,72 @@
# 入库异步任务化 Spec
## Why
当前 `POST /api/v1/documents` 同步等待整条入库流水线(总结→分类→切分→向量化→写库),真实 Ollama 冒烟单篇耗时 39s+,长文档分钟级,客户端/网关易超时,批量导入不可用。改为后台任务模式:提交即返回 task_id,状态可查,失败任务长期保留便于归因。
## What Changes
- **BREAKING**`POST /api/v1/documents` 由同步返回 IngestionResult200)改为立即返回 `{task_id, status}`(HTTP 202),流水线在后台 asyncio 任务中执行
- **新增** `GET /api/v1/documents/tasks/{task_id}`:查询任务状态(pending/各阶段/done/failed)、完成后的 IngestionResult、失败时的 stage 与错误信息
- **新增** `app/core/ingest_tasks.py` IngestTaskManager:提交登记 → 信号量限流并发 → 后台执行 → 状态机推进 → 结果/错误落储
- **状态存储**Redis 为主(key `ingest_task:{task_id}`JSON);done TTL 24h**failed TTL 7 天**(便于失败归因);Redis 不可用时降级进程内字典(重启丢失,日志告警),服务不因此拒绝入库
- **管理页**:入库区块改为「提交 → 轮询任务状态 → 展示进度/结果/失败详情」
- **配置新增**`ingest_max_concurrency`(默认 2)、`ingest_task_ttl_done`(默认 86400s)、`ingest_task_ttl_failed`(默认 604800s7 天)
- CLAUDE.md API 清单同步更新(标注 POST /documents 行为变更)
## Impact
- Affected specs: 文档入库 API、管理页面、任务状态存储
- Affected code:
- 修改:[document.py](file:///Users/kplam/coding/QMDSearch/app/api/v1/document.py)POST 改 202 + 新增任务查询端点)、[config.py](file:///Users/kplam/coding/QMDSearch/app/config.py)、[.env.example](file:///Users/kplam/coding/QMDSearch/.env.example)、[admin.html](file:///Users/kplam/coding/QMDSearch/app/static/admin.html)(入库区块轮询)、[CLAUDE.md](file:///Users/kplam/coding/QMDSearch/CLAUDE.md)
- 新增:`app/core/ingest_tasks.py``tests/test_ingest_tasks.py``tests/test_ingest_task_api.py`
## ADDED Requirements
### Requirement: 异步入库提交
系统 SHALL 将 `POST /api/v1/documents` 改为异步任务模式:校验通过后立即返回 HTTP 202 与 `{task_id, status: "pending"}`,入库流水线在后台任务执行;并发后台任务数受 `ingest_max_concurrency` 信号量限制,超限任务保持 pending 排队。请求体与校验规则(text 非空等)与现状一致,校验失败仍同步返回 1001。
#### Scenario: 提交即返回
- **WHEN** 提交合法文档
- **THEN** 响应 202 + task_id,无需等待流水线完成;后台任务按并发额度开始执行
### Requirement: 任务状态查询
系统 SHALL 提供 `GET /api/v1/documents/tasks/{task_id}`:返回 `{task_id, status, created_at, updated_at}`status 取值 `pending | summarizing | classifying | embedding | writing | done | failed`(与流水线阶段一致推进);done 时附 `result`(完整 IngestionResultdocument_id/category/tags/总结层级/chunks_count);failed 时附 `error: {stage, message}`stage 来自 IngestionError,已产出总结不丢失一并附在 error.partial_summary)。task_id 不存在返回 code=1004。
#### Scenario: 生命周期
- **WHEN** 提交后轮询 task_id
- **THEN** 依次观察到 pending→阶段状态→done(result 可查,文档已可检索);失败任务观察到 failed + error.stage
### Requirement: 状态存储与保留
任务状态 SHALL 写入 Redis`ingest_task:{task_id}`JSON):done TTL=ingest_task_ttl_done(默认 24h),failed TTL=ingest_task_ttl_failed(默认 7 天),进行中状态 TTL 取 done 值。Redis 不可用时降级进程内字典并日志告警,入库与查询照常(重启后历史任务丢失可接受,接口语义不变)。
#### Scenario: Redis 宕机降级
- **WHEN** Redis 连接失败
- **THEN** 提交与查询仍正常,仅日志 warning;进程重启后任务状态丢失
### Requirement: 管理页入库区块
管理页入库表单 SHALL 改为:提交后展示 task_id 与状态进度条/文本,每 2s 轮询任务接口,done 展示分类/标签/总结层级/chunks_countfailed 展示 error.stage 与 message;轮询期间禁止重复提交,超时(5 分钟)停止轮询并提示可稍后手动查询。
### Requirement: 并发控制
后台入库 SHALL 用 asyncio 信号量限制并发(默认 2),避免多任务同时打满 Ollama/embedding;排队任务状态保持 pending。
## MODIFIED Requirements
### Requirement: POST /api/v1/documents(行为变更)
原同步返回 IngestionResult 的行为废弃,改为 202 + task_id。所有既有调用方(管理页、既有测试)同步适配;响应体统一响应包装不变(`{code:0, data:{task_id,status}, message:"ok"}`HTTP 状态码 202)。
## REMOVED Requirements
### Requirement: 同步入库等待
**Reason**:长耗时流水线导致客户端超时、无法批量导入。
**Migration**:调用方改为提交后轮询 `GET /api/v1/documents/tasks/{task_id}`;管理页已内置轮询。