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

5.4 KiB
Raw Permalink Blame History

多格式文档支持 — 设计文档

日期: 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

PDFSplitterHTMLSplitter 不继承 BaseTextSplitter,而是组合一个 TextSplitter 实例。它们实现 Splitter Protocol,在 split() 中:提取纯文本 → 委托 TextSplitter.split()

注册表

registry.py 维护默认扩展名映射:

_DEFAULT_MAP = {
    ".md": "markdown",
    ".markdown": "markdown",
    ".txt": "text",
    ".pdf": "pdf",
    ".html": "html",
    ".htm": "html",
}

def get_splitter(file_path: str, **config) -> Splitter:
    """根据扩展名自动选择 Splitter,未匹配回退到 TextSplitter."""

依赖策略

  • pymupdfbeautifulsoup4 作为可选依赖
  • 首次使用 PDF/HTML 格式时才 import,库缺失时抛 ImportError 带安装提示
  • 安装方式:
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

def ingest_file(self, file_path: str) -> int:
    splitter = self.splitter or get_splitter(file_path,
        max_size=..., overlap=...)
    ...

ingest_directory()

rglob("*.md") 改为遍历所有支持格式:

_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:零改动。ingestingest-dir 自动获得多格式能力
  • API:零改动。POST /api/v1/ingestfile_path 自动支持
  • ingest_content():仍默认使用 MarkdownSplitter(处理 Markdown 字符串),显式传入 content 时不变

风险

风险 缓解
PDF 提取失败(扫描件/图片 PDF) 抛明确异常,跳过该文件
HTML 标签复杂导致去标签不干净 soup.get_text(separator="\n") 保留段落结构
ingest.py 迁移 MarkdownSplitter 后旧 import 路径失效 ingest.py 保留兼容 importfrom src.core.splitters.markdown import MarkdownSplitter