数据截至 (上游 commit 8c51b8dc5408)
Morphik Core — 架构与原理
30 秒导读: Morphik Core 是一个开源的多模态检索引擎。它和传统 RAG 最大的不同是:处理 PDF/PPT/Word 这类"看的比读的重要"的文档时,它不把文档抽成纯文本,而是把每一页渲染成图片,用视觉模型 ColPali 建索引;命中后把那一页的原图送给大模型看图作答。图表、版式、手绘标注因此不会在"抽文本"这步丢掉。
1. 这是什么(零基础也能懂)
一句话定义
一个可自托管的文档检索后端:你把文件丢进去,它负责解析、切块、向量化、存储;你提问,它负责找出相关内容并调大模型给答案。
它想解决的问题
先看传统 RAG 的做法:PDF → 抽文本 → 切块 → 文本向量 → 相似度检索。
这套流程在视觉密集的文档上会塌:
| 文档里的东西 | 抽成文本后变成什么 |
|---|---|
| 柱状图 / 折线图 | 几个孤立的数字,失去坐标轴含义 |
| 工程图纸的尺寸标注 | 散落的字符串,失去空间关系 |
| 跨页表格 | 一串没有行列结构的文字 |
| 扫描件 / 手写批注 | OCR 噪声,或干脆是空 |
Morphik 的回答是:别抽了,直接看。把每一页原样渲染成图片交给视觉模型建索引。
给谁用
- 要在合同、图纸、财报、说明书上做问答的开发者。
- 需要自托管(数据不出内网)且要能换掉每个组件(嵌入模型、向量库、对象存储)的团队。
用起来什么样
下面是官方 Python SDK 的最小用法(源自 README.md 与 sdks/python/morphik/sync.py:1125 的 retrieve_chunks 签名):
from morphik import Morphik
morphik = Morphik("<your-morphik-uri>")
# 摄取:异步排队,函数很快返回,后台 worker 慢慢处理
morphik.ingest_file("chair-assembly-manual.pdf")
# 问答:内部会先检索、再把命中页交给大模型
answer = morphik.query("14-A 号螺丝的长度是多少?")
# 只要检索结果、不要生成:padding=1 表示连命中页的前后各一页一起返回
chunks = morphik.retrieve_chunks("扭矩规格表", k=5, use_colpali=True, padding=1)
注意 use_colpali=True 这个开关——它决定了走"看图"这条路还是走传统文本向量那条路。这是整个项目最重要的一个参数。
一句话直觉
把每一页文档当成一张截图,把检索当成"在一堆截图里找出最像回答这个问题的那几张"。 找到之后不做转述,直接把截图递给会看图的模型。
2. 顶层全景(它大概怎么转)
两个面:写入是异步的,读取是同步的
这张图从上到下读。上半部分是写入面(摄取),下半部分是读取面(查询)。两个面之间只通过数据库和向量库耦合。
========================= 写入面(异步,可能几分钟) =========================
上传文件
|
v
FastAPI /ingest/file ---> 原文件落对象存储 (S3 / 本地盘)
| 文档行落 Postgres,status = processing
v
Redis 队列 (arq) <-- 立刻返回 Document 给调用方
|
v
ingestion worker ---> 解析 -> 切块 -> 向量化 -> 写向量库
最后把 status 改成 completed
========================= 读取面(同步,亚秒到数秒) =========================
提问
|
v
FastAPI /query ---> ① 先查"这个租户能看哪些文档"(拿到 doc_ids 白名单)
| ② 同时把问题编码成查询向量
v
向量检索(限定在白名单内)---> 命中若干 chunk(可能是页面图片)
|
v
组装上下文:文本原样,图片转成 image_url
|
v
视觉大模型 ---> 答案 + 出处(文件名 + 页码)
怎么读这张图: 写入面和读取面唯一的交汇点是"向量库 + Postgres 文档表"。写入没完成(status 不是 completed)的文档,读取面根本查不到——过滤条件写死在 core/services/document_service.py:340 的 status_filter=["completed"]。
部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| FastAPI app | 对外 HTTP 接口,/ingest/*、/retrieve/*、/query | core/api.py:179、core/routes/ |
| 服务单例装配 | 按配置把数据库、存储、解析器、模型、向量库拼起来 | core/services_init.py |
| IngestionService | 落库 + 上传 + 入队,不做重活 | core/services/ingestion_service.py:60 |
| ingestion worker | arq 后台进程,干解析/向量化/写库的重活 | core/workers/ingestion_worker.py:448 |
| MorphikParser | 文件 → 文本(Docling / openpyxl / 视频 / XML 多路) | core/parser/morphik_parser.py:196 |
| ColpaliEmbeddingModel | 页面图片 / 文本 → 多向量(每 token 一个 128 维向量) | core/embedding/colpali_embedding_model.py:25 |
| MultiVectorStore | 多向量的 Postgres 实现(位串 + max_sim SQL 函数) | core/vector_store/multi_vector_store.py:39 |
| FastMultiVectorStore | 多向量的快路实现(定长编码 + Turbopuffer ANN + 精确重排) | core/vector_store/fast_multivector_store.py:305 |
| PGVectorStore | 传统单向量 pgvector 存储(非 ColPali 路径用) | core/vector_store/pgvector_store.py |
| DocumentService | 检索与问答的主逻辑 | core/services/document_service.py:43 |
| LiteLLMCompletionModel | 把上下文(含图片)喂给任意厂商的大模型 | core/completion/litellm_completion.py:217 |
主线走一遍(高层,不进代码)
摄取一份 PDF:
- HTTP 收到文件 → 存进对象存储 → Postgres 建一行
status=processing→ 往 Redis 塞一个任务 → 立即返回。 - worker 取出任务 → 从对象存储下载文件。
- 判断是不是 ColPali 原生格式(PDF/图片/Word/PPT)。是的话跳过文本解析,直接把每页渲染成 PNG。
- 每页图片过 ColPali 模型,得到一组多向量。
- 按批(默认 16 页)写进多向量库,页面图片本体写对象存储。
status改成completed,文档可被检索。
回答一个问题:
- 鉴权,得到
app_id(云上)或user_id(自托管)。 - 并行做两件事:把问题编码成查询多向量;查 Postgres 拿到"当前租户 + 过滤条件下可见且已完成"的文档 ID 列表。
- 在这个 ID 列表范围内做多向量检索,拿回 top-k 个页面。
- 可选:padding(把命中页的前后几页也捞出来当上下文)。
- 页面图片转成 data URI,连同文本块一起塞进 LLM 请求,图片走
image_url通道。 - 返回答案 +
sources(文档 ID、chunk 号、分数)。
3. 一眼看懂的关键设计选择
这几条是理解全项目的钥匙,细节在各章展开:
| 选择 | 具体是什么 | 后果 |
|---|---|---|
| 页即块 | ColPali 路径下,PDF 的第 N 页就是 chunk_number = N | 引用可以精确到页;渲染失败也要放一张白页占位以免页号错位 |
| 二选一,不混合 | 一个文档要么进 ColPali 多向量库,要么进 pgvector,不会两边都进 | 省一半算力和存储;代价是同一批文档无法真正混合检索 |
| 内容不进数据库 | chunk 的真实内容(页面 PNG、文本)存对象存储,库里只存 key | 数据库轻;代价是检索后要多一轮下载 |
| 摄取全异步 | HTTP 只负责排队,重活全在 arq worker | 上传大文件不会超时;代价是要自己轮询 status |
| 先粗后精 | 快路先用一个定长向量做 ANN 粗筛,再用完整多向量精确打分 | 兼顾召回与延迟,是快路的核心把戏 |
4. 阅读地图
建议顺序就是编号顺序;只想解决具体问题的话按下表跳。
| 章节 | 讲什么 | 什么时候读它 |
|---|---|---|
| 01-ingestion-pipeline.md | 摄取六步、解析降级阶梯、批处理与重试 | 想知道"我的文件为什么卡在 processing" |
| 02-colpali-visual-retrieval.md | ColPali 是什么、MaxSim 在算什么、模型怎么调 | 想理解这个项目的核心创新 |
| 03-multivector-stores.md | 两套多向量存储的原理和取舍 | 要选型,或觉得检索太慢 |
| 04-retrieval-and-answering.md | 检索四种配置、权限过滤、padding、多模态提示词 | 想调检索效果 |
| 05-storage-tenancy-ops.md | 存储分层、多租户、进度与指标 | 要部署或排障 |
| 06-design-notes.md | 值得借鉴的技巧、已知边界、横向对比、总代码地图 | 读完想带走点什么 |
5. 代码地图(入口级)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 进程入口 | start_server.py | (脚本主体) |
| FastAPI 应用与生命周期 | core/api.py、core/app_factory.py | app、lifespan |
| 全部单例的装配处 | core/services_init.py | document_service、ingestion_service、colpali_vector_store |
| 配置加载(toml → Settings) | core/config.py | Settings、get_settings |
| 中央配置文件 | morphik.toml | [morphik]、[multivector_store]、[parser] |
| 摄取入口 | core/routes/ingest.py | ingest_file、ingest_text |
| 后台任务与 worker 配置 | core/workers/ingestion_worker.py | process_ingestion_job、WorkerSettings |
| 检索与问答主逻辑 | core/services/document_service.py | retrieve_chunks、query |
| ColPali 嵌入模型 | core/embedding/colpali_embedding_model.py | ColpaliEmbeddingModel |
| 多向量存储(慢路 / 快路) | core/vector_store/multi_vector_store.py、core/vector_store/fast_multivector_store.py | MultiVectorStore、FastMultiVectorStore |