143 lines
5.4 KiB
Markdown
143 lines
5.4 KiB
Markdown
# 多格式文档支持 — 设计文档
|
||
|
||
**日期**: 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` |
|