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