Files
md-vector-db/docs/superpowers/specs/2026-07-10-multi-format-support-design.md

143 lines
5.4 KiB
Markdown
Raw Permalink 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.
# 多格式文档支持 — 设计文档
**日期**: 2026-07-10
**版本**: 1.0
## 目标
将 md-vector-db 从"仅 Markdown"扩展为支持 `.txt``.pdf``.html` 的多格式文档向量数据库。
## 非目标
- 不支持 `.docx``.pptx``.epub` 等 Office/电子书格式(留待后续扩展)
- 不改变现有的嵌入和检索流程
- 不改变 HTTP API 的请求/响应模型
## 架构
### 模块结构
```
src/core/
├── splitters/ # 新建目录
│ ├── __init__.py # 导出 registry + 所有 Splitter
│ ├── base.py # Splitter Protocol + BaseTextSplitter (ABC)
│ ├── markdown.py # MarkdownSplitter (从 ingest.py 移入)
│ ├── text.py # TextSplitter (纯文本按段落+标点硬切)
│ ├── pdf.py # PDFSplitter (pymupdf → TextSplitter)
│ ├── html.py # HTMLSplitter (bs4 → TextSplitter)
│ └── registry.py # 工厂 + 扩展名→Splitter 映射表
├── ingest.py # 精简,用 registry.get_splitter() 自动选择
```
### 类继承
```
Splitter (Protocol)
└── BaseTextSplitter (ABC) # max_size, overlap, _split_single_paragraph
├── MarkdownSplitter # 已有,从 ingest.py 移入
├── TextSplitter # 纯文本:按 \n\n 切段落,超长按标点硬切
├── PDFSplitter # 读取 PDF → extract_text() → 委托 TextSplitter
└── HTMLSplitter # 读取 HTML → get_text() → 委托 TextSplitter
```
`PDFSplitter``HTMLSplitter` **不继承** `BaseTextSplitter`,而是组合一个 `TextSplitter` 实例。它们实现 `Splitter` Protocol,在 `split()` 中:提取纯文本 → 委托 `TextSplitter.split()`
### 注册表
`registry.py` 维护默认扩展名映射:
```python
_DEFAULT_MAP = {
".md": "markdown",
".markdown": "markdown",
".txt": "text",
".pdf": "pdf",
".html": "html",
".htm": "html",
}
def get_splitter(file_path: str, **config) -> Splitter:
"""根据扩展名自动选择 Splitter,未匹配回退到 TextSplitter."""
```
## 依赖策略
- `pymupdf``beautifulsoup4` 作为**可选依赖**
- 首次使用 PDF/HTML 格式时才 `import`,库缺失时抛 `ImportError` 带安装提示
- 安装方式:
```bash
uv sync --extra pdf # PDF 支持
uv sync --extra html # HTML 支持
uv sync --extra all # 全部可选依赖
```
## ingest.py 改动
### DocumentIngestor.__init__
已有 `splitter` 可选参数,**保持不变**。显式传入的 splitter 覆盖自动选择。
### ingest_file()
改为使用 `get_splitter(file_path, ...)` 自动选择 splitter
```python
def ingest_file(self, file_path: str) -> int:
splitter = self.splitter or get_splitter(file_path,
max_size=..., overlap=...)
...
```
### ingest_directory()
`rglob("*.md")` 改为遍历所有支持格式:
```python
_SUPPORTED_SUFFIXES = {".md", ".markdown", ".txt", ".pdf", ".html", ".htm"}
def ingest_directory(self, dir_path: str) -> dict[str, int]:
for f in Path(dir_path).rglob("*"):
if f.suffix.lower() in _SUPPORTED_SUFFIXES:
...
```
## 数据流
```
文件路径 → get_splitter(path)
├── .md → MarkdownSplitter.split(text)
├── .txt → TextSplitter.split(text)
├── .pdf → PDFSplitter.split(binary)
│ └── pymupdf 提取文字 → TextSplitter.split(text)
└── .html → HTMLSplitter.split(html)
└── bs4 去标签 → TextSplitter.split(text)
batch_embed(chunks)
ChromaDB.add()
```
## 测试策略
- `tests/test_splitters_text.py` — TextSplitter 段落切分、硬切、overlap
- `tests/test_splitters_registry.py` — 扩展名映射、回退逻辑、自定义注册
- `tests/test_splitters_pdf.py` — PDF 提取文字 + 分块(需 pymupdf)
- `tests/test_splitters_html.py` — HTML 去标签 + 分块(需 bs4
- `tests/test_ingest.py` — 补 `ingest_directory` 多格式遍历测试
## CLI/API 影响
- **CLI**:零改动。`ingest``ingest-dir` 自动获得多格式能力
- **API**:零改动。`POST /api/v1/ingest``file_path` 自动支持
- **`ingest_content()`**:仍默认使用 `MarkdownSplitter`(处理 Markdown 字符串),显式传入 content 时不变
## 风险
| 风险 | 缓解 |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| PDF 提取失败(扫描件/图片 PDF) | 抛明确异常,跳过该文件 |
| HTML 标签复杂导致去标签不干净 | 用`soup.get_text(separator="\n")` 保留段落结构 |
| `ingest.py` 迁移 MarkdownSplitter 后旧 import 路径失效 | 在`ingest.py` 保留兼容 import`from src.core.splitters.markdown import MarkdownSplitter` |