项目落地跑了三个月,我被问得最多的不是"大模型怎么选",而是"RAGFlow 存的东西到底在哪里""为什么有时候检索慢""容器占了几十G空间到底装了什么"。这些问题本质上都指向同一个主题:RAGFlow 的存储架构。它不像传统 Web 项目那样只有一个数据库,而是把数据拆成了四层来管——元数据、对象、检索、缓存,各自负责一类活,又互相配合。这篇我就把这四层拆开讲清楚,结合我实际部署和排查问题的经历,说明每一层存了什么、为什么这么设计、以及它们之间是怎么协作的。
这套架构不是什么花架子。RAGFlow 的核心是文档解析和检索增强,一个 PDF 进来之后,要经历文件落地、内容抽取、向量化、索引构建、查询召回这么一串流程,每一环节的数据形态和读写特征完全不一样。用单一存储硬扛,要么性能拉胯,要么数据管理混乱。四层分工本质上是把"不同类型的数据"放到"最擅长处理它的组件"里,这是 RAG 系统存储设计的基本逻辑。如果你正在搭 RAG 服务,或者准备在生产环境用 RAGFlow,这篇文章能帮你理解数据到底怎么流转,排查问题的时候也更有方向。
1. 为什么要拆四层存储:从一次失败的检索说起
先说一个我真实遇到的场景。客户上传了一批 PDF 合同,问"去年跟 A 公司签的付款条款是什么",结果系统压根没召回相关内容。我当时第一反应是 embedding 模型的问题,查了一圈发现不是,最后定位到是 chunk 文本压根没进检索库。为什么没进?因为文档解析任务虽然在界面上显示成功,但 MySQL 里的任务状态和 ES 里的索引数据出现了不一致,解析完成的回调没触发索引写入。
这个问题的根源就是:RAGFlow 里"文档"这个概念,实际上分散在四个地方管理。存在 MinIO 里的原始 PDF 是一份,MySQL 里记录文件元数据和解析任务是一条,Elasticsearch 里可检索的 chunk 是另一批,Redis 里还存着任务状态和会话缓存。任何一个环节不同步,表现出来就是"文件上传了但检索不到""解析成功但问答没效果"。
所以理解四层存储,不只是搞懂架构图,更是排查问题的基本功。你至少要知道:一个文件进来,每一层分别做了什么,哪一层出了问题会有什么现象。
1.1 单库时代的尴尬
如果只用一个关系型数据库来存 RAG 系统的所有数据,会出现三个很现实的问题。
第一个是二进制大对象的问题。一份带扫描图片的 PDF 可能上百 MB,如果直接塞进 MySQL,光读写就够受的,备份也会变成噩梦。更麻烦的是,RAG 系统经常要做文档版本对比、原文件预览这些操作,这些场景需要的是"直接按路径取文件",而不是"从数据库里查出来再拼成文件"。
第二个是检索性能的问题。向量检索本质上是高维空间里的最近邻搜索,传统数据库的 B+ 树索引对这个场景完全无能为力。几十万条 chunk 数据做暴力扫描,延迟直接到秒级,这在问答场景里是不可接受的。
第三个是读写模式冲突的问题。元数据是小数据量的高频读写,文件对象是大数据量的低频读写,向量索引是写入一次但查询极其频繁,缓存是超高频的临时读写。把这些不同特征的数据混在一个存储里,性能调优会变得非常拧巴——你调了连接数,可能影响的是元数据查询;你调了缓存大小,可能挤占了文件存储的空间。
RAGFlow 的四层设计,本质上是把这个"什么都想做"的单体存储,拆成了四个各司其职的组件。这个思路跟微服务拆分的逻辑一模一样:与其在一个系统里平衡所有需求,不如让每个系统专心做好一件事。
1.2 四层架构的分工逻辑
RAGFlow 的四层存储各有明确的分工范围。
第一层是 MySQL,管元数据。包括知识库配置、文档基本信息、解析任务状态、chunk 与文档的映射关系、用户权限和会话历史。这一层的特点是:数据量不大,但关系复杂,需要事务支持,允许通过 SQL 灵活查询。
第二层是 MinIO,管对象。包括上传的原始文件(PDF、DOCX、图片)、解析过程中生成的辅助文件、可能有的图片裁剪结果。这一层的特点是:存的是二进制大对象,按路径访问,不需要事务,但要求可靠持久化。
第三层是 Elasticsearch,管检索。包括解析后的 chunk 文本、向量化之后的 embedding、用于全文检索的倒排索引。这一层的特点是:写入路径固定(解析完成后写入),但查询路径极其频繁,延迟要求毫秒级。
第四层是 Redis,管缓存。包括任务执行状态的临时标记、拆分成多个子任务后的进度汇总、问答会话的上下文缓存、可能的限流计数。这一层的特点是:数据生命周期短、读写超高频、允许丢失后重建。
四层之间不是孤立的。元数据层和对象层之间有引用关系,检索层的数据来源是对象层解析的结果,缓存层则服务于所有层的并发协调。理解了这个依赖关系,才能真正看懂 RAGFlow 的部署架构。
2. 元数据存储层:MySQL 才是真正的"大脑"
很多人以为 RAGFlow 的核心是 Elasticsearch,因为检索是重点。但从数据管理的角度讲,MySQL 才是整个系统的大脑。知识库列表、文件清单、任务调度、解析进度,全部由 MySQL 统一管理。
2.1 元数据到底存了什么
RAGFlow 的 MySQL 数据库里,最核心的是几类表:知识库表、文档表、任务表,以及文档与 chunk 的关联关系表。我用实际数据来举例。
知识库表记录的是"用户创建了一个叫'合同库'的知识库",包括知识库的 ID、名称、embedding 模型配置、检索参数设置(比如 topK 默认值、相似度阈值)。文档表记录的是"合同库下上传了'2024年度采购合同.pdf'"这个文件的基本信息,包括文件名、文件大小、文件类型、上传时间、解析状态。任务表记录的是"这个文档的解析任务被拆分成了哪几个子任务、每个子任务跑在哪个节点、当前进度是多少、失败了是什么错误信息"。
这里有一个容易被忽略的点:chunk 的文本本身不存 MySQL,但 chunk 的 ID、所属文档 ID、在源文件中的页码位置、在知识库中的顺序号,这些"关于数据的数据"是存在 MySQL 里的。检索阶段从 ES 拿到了命中的 chunk ID 列表之后,系统会回到 MySQL 查这些 chunk 的关联信息,比如去定位原始文档、去拼装引用来源。
这个设计的原因很现实:ES 擅长检索但不太适合做关系查询,MySQL 擅长关系管理但不太适合向量检索。把 chunk 的内容和 chunk 的元信息分开放,各自用最合适的方式管理,这是混合存储架构里非常典型的做法。
2.2 元数据与文件和索引的绑定关系
元数据层最关键的价值,是它作为文件对象和检索索引之间的桥梁。这个桥梁关系如果不理解,排查"文件上传了但检索不到"这类问题就容易抓瞎。
一个文档在系统中的完整 ID 链条是这样的:知识库 ID 标识"属于哪个知识库",文档 ID 标识"是哪一次上传的文件",任务 ID 标识"解析进程在哪一个批次",chunk ID 标识"解析出来的第几段文本"。四个 ID 逐层绑定,MySQL 里维护着这些绑定关系。
实际排查中,我发现很多问题的根因都在这个绑定链路上。比如上传了一个 20MB 的 PDF,解析任务失败在某个子任务上,但界面显示"已上传"而不是"解析失败"——这是因为文档表的"上传状态"和"解析状态"是两个字段,上传成功不代表解析完成。再比如同一个文件删了重新上传,生成了新的文档 ID,但旧 chunk 还残留在 ES 里,如果不清理,检索时会出现"明明删了还能搜到"的诡异现象。
所以我对使用 RAGFlow 的朋友有一个建议:把 MySQL 里的 document 表和 task 表当作排查问题的起点。文件上传了没有?解析任务成功没有?chunk 生成没有?三个问题依次确认,60% 以上的检索异常都能定位到环节。
3. 对象存储层:MinIO 撑起大文件底座
MinIO 在 RAGFlow 里管的是最重的数据:原始文件。你可能觉得"存个文件而已,为什么还要单独部署一个服务",但实际用下来就明白,文件存储远远不是"把字节写进磁盘"那么简单。
3.1 为什么不能把文件塞进数据库
从 MySQL 的角度看,把 PDF 文件塞进数据库意味着每条记录都带一个巨大的 BLOB 字段。这会导致三个问题:一是表空间膨胀严重,备份和迁移的时间成本暴增;二是查询性能下降,即使只是查文件的元信息,也要扫描大字段;三是应用层的数据处理复杂度上升,跨机器的文件共享变得困难。
RAGFlow 选择 MinIO 是更合理的路径。MinIO 本身就是为对象存储设计的服务,采用 S3 兼容协议,文件以对象形式存放在桶(Bucket)里,用路径即可访问。它在 RAGFlow 里的角色很像网盘:只管文件的存放和读取,不关心文件内容是什么含义。
还有一层原因和容器化部署有关。RAGFlow 官方推荐用 docker compose 部署,MinIO 作为独立服务,它的数据目录单独挂载在宿主机上。这带来的好处是:哪怕 RAGFlow 应用容器需要升级重建,原始文件数据也不会丢失。这个特性让我在多次升级 RAGFlow 版本的时候非常安心,不用像以前单体应用那样担心升级导致用户上传资料丢失。
3.2 RAGFlow 中的对象流转过程
MinIO 在 RAGFlow 里承接的对象流转,大概分成三个阶段。
第一阶段是上传落地。用户上传 PDF 时,文件先写入 MinIO 的指定桶,写入成功后 MySQL 里才生成文档记录。这个顺序很关键:先有文件,后有元数据,避免出现"数据库有记录但文件找不到"的悬挂引用。
第二阶段是解析读取。文档解析任务启动后,解析服务从 MinIO 拉取原始文件,在内存或临时目录中做版面分析、OCR 识别、文本抽取。解析完之后,有时候还需要把中间产物(比如裁剪出的表格图片)存回 MinIO,供后续"看原文"功能引用。
第三阶段是预览回读。用户在前端点击"查看原始文档"时,后端不经过 MySQL,直接向 MinIO 发起文件读取请求,通过预签名 URL 的方式把文件流返回给浏览器。这一步绕开数据库的好处是,大文件的传输不会占用 MySQL 的连接和内存。
我遇到过的一个坑是:MinIO 的访问凭证到期后,解析任务会批量失败,但界面提示又不够明显。后来我在检查容器日志的时候才发现,报错信息里全是"AccessKeyId does not exist"。所以如果你把 RAGFlow 数据目录整个迁移到新机器,一定要记得检查 MinIO 的部署配置里是否有持久化的环境变量配置,否则容器重建后凭证信息重置,旧数据就"看不见"了。
4. 检索存储层:Elasticsearch 的混合检索战场
如果说 MySQL 是大脑,Elasticsearch 更像 RAGFlow 的"检索肌肉"。所有解析出来的文本块和向量,最终都要在 ES 里完成索引构建,用户的问题也要在这里做召回。这一层对性能的影响最直接,也是优化空间最大的地方。
4.1 索引数据结构和 Mapping
RAGFlow 在 ES 里索引的数据,每个文档(在 ES 语义下)对应一个 chunk。chunk 包含几个关键字段:chunk 的 ID、所属文档的 ID、所属知识库的 ID、chunk 的文本内容、chunk 的向量值(embedding 数组),以及可能有的页码和位置信息。
这些字段的 Mapping 设计是有讲究的。文本字段需要配置分词器,中文环境一般用 IK 分词器,这样"付款条款"可以被切分成有意义的词元;向量字段则声明为 dense_vector 类型,维度和 embedding 模型的输出维度一致(常见的如 768 维或 1024 维),同时要配置相似度度量方式,RAGFlow 默认用余弦相似度来做向量距离计算。
这里有一个容易忽视的细节:向量维度不是随意设置的,它必须和 embedding 模型对齐。如果你切换了 embedding 模型,新模型的向量维度变了,旧索引里的向量数据就全部失效了,必须重建索引。我有一次在界面上切换了模型,结果检索一直返回空,折腾了半天才意识到是维度不匹配导致查询报错。
ES 的索引在 RAGFlow 里是按 embedding 模型维度来组织管理的。每个模型对应一套索引,同一个模型下的多个知识库共享索引,用知识库 ID 字段来做过滤。这种设计避免了"每个知识库建一个索引"带来的资源浪费,因为 ES 的索引数量太多会严重影响集群的资源调度和写入性能。
4.2 从向量召回到底层重排
用户发起查询时,ES 上的操作比想象中复杂。首先,用户的自然语言问题会被同一个 embedding 模型向量化,生成一个查询向量。然后,ES 会同时执行两条检索路径:一条是向量检索,用 HNSW 算法在高维空间里找最近的 topN 个向量;另一条是全文检索,用户问题里的关键词去匹配倒排索引。两条路径的结果合并去重后,进入重排阶段。
重排(Rerank)是 RAGFlow 检索质量的关键。初步召回的结果可能有一两百条,但真正和用户问题语义高度相关的可能只有前几条。RAGFlow 会用一个交叉编码器模型(比如 bge-reranker)对候选 chunk 逐条打分,把分数最高的几条(通常是 topK 条)作为最终结果返回给大模型。
这里要提一个和四层存储有关的点:ES 召回的结果,只包含初步的相似度分数,但 RAGFlow 会从 MySQL 补全 chunk 的元信息(比如原文页码、所属文档标题),从 MinIO 关联原始文件的预览地址。最终呈现给用户的引用来源,实际上是 MySQL、ES、MinIO 三层数据拼装出来的成果。如果引用来源显示异常,排查重点不是 ES,而是元数据层的关联关系是否完整。
检索层的优化心得:不要一上来就调 embedding 模型。先确认 ES 的分片数量和副本数是否合理,再确认查询语句里有没有加知识库过滤条件,最后再考虑调整 topK 和相似度阈值。很多"检索不准"其实是配置问题,不是模型问题。
5. 缓存层:Redis 在 RAGFlow 里到底缓存了什么
Redis 在 RAGFlow 里的存在感最低,容易被忽略,但它撑起了整个系统的并发协调和会话管理。没有 Redis,大批量文件上传和解析时会出各种并发问题。
5.1 会话缓存与并发控制
RAGFlow 的文档解析是一个长耗时任务。一个几十 MB 的 PDF,从上传到解析完成可能要好几分钟。RAGFlow 会把这种长任务拆分成多个子任务并行处理,子任务的进度需要实时汇总。
这时候 Redis 的价值就体现出来了。子任务在 Redis 里写入进度标记,主任务定期读取所有子任务的进度,在界面上展示"正在解析第 3/10 块"。这种设计比直接写在 MySQL 里高效得多,因为进度更新是超高频的写操作——如果每个子任务的每一条进度都要更新 MySQL 字段,数据库的写入压力会非常大,还可能造成行锁竞争。
Redis 还承担了会话上下文的缓存。用户在对话界面里的临时上下文、正在进行的回答状态、等待大模型流式输出时的中间结果,都存在 Redis 里。这些数据的生命周期很短,回答结束后就过期删除,不需要长期持久化。
限流和任务队列的协调也是 Redis 的活。RAGFlow 在并行处理任务时会控制同时运行的数量,避免一次性把所有子任务全部塞给解析服务导致内存溢出。这个协调逻辑用 Redis 的计数器来实现非常方便,还能避免多个 worker 节点同时抢任务造成冲突。
5.2 缓存失效与一致性问题
Redis 缓存有一个天然问题:数据不是强一致的。在 RAGFlow 里,这个问题在"删除知识库""切换模型"这类操作后尤为明显。
举个例子:一个知识库里有 100 个文档,全部解析完并建立了索引。如果用户删除这个知识库,MySQL 里的元数据会删掉,ES 里的索引文档可能也会删掉,但 Redis 里如果还残留着跟这个知识库相关的缓存 key,内存里就有了"死数据"。这些数据占用的内存不大,但如果不定期清理,日积月累也会造成内存碎片。
另外一个常见的缓存问题是会话状态串台。多用户同时使用 RAGFlow 时,Redis 的 key 设计里必须包含会话 ID。我排查过一个"用户 A 的问答案例出现在用户 B 的聊天记录里"的问题,最后发现是 Redis 里 session key 的过期时间设置过长,导致旧会话没有被及时清理,新会话复用了同一个 key。
我的经验是:给 Redis 设置合理的内存淘汰策略,比如 allkeys-lru,让不常用的缓存自动被淘汰。RAGFlow 的缓存数据基本都是可重建的,丢失了重新跑一次任务就行,不需要保护性缓存。
6. 四层协作全链路:一个文件从上传到回答的完整旅程
四层存储分开讲完了,但实际的系统运行是四层联动。我始终觉得,理解了全链路的数据流转,才算真正看懂了 RAGFlow。这一节我以一份 PDF 合同为例子,完整走一遍从上传到最终问答的流程。
6.1 上传与解析阶段
第一步,用户把 PDF 上传到 RAGFlow 前端,文件流先进入 nginx,再由后端转发到 MinIO,落盘到指定桶。这一步完成后,MySQL 的 document 表插入一条记录,状态标记为"已上传待解析"。
第二步,后端创建解析任务,向消息队列投递任务消息。任务消息里携带文档 ID 和存储路径,因为文件本体已经在 MinIO 里了,消息不需要携带文件内容。这一步就是元数据层和对象层的第一次协作:MySQL 提供文档 ID 作为引用,MinIO 提供文件读取路径。
第三步,解析 worker 节点从消息队列拉取任务,从 MinIO 读取原始文件。解析过程包括版面分析、段落切分、OCR 识别、表格提取,最终得到一批 chunk 文本。每个 chunk 带着自己的页码、坐标和文档 ID,这些信息之后会写进 MySQL 和 ES。
第四步,解析完成后,worker 调用 embedding 模型为每个 chunk 生成向量。然后把这些向量连同文本一起批量写入 ES。写入完成后,通过 Redis 上报任务进度,MySQL 的 document 表更新解析状态为"完成"。
6.2 索引与检索阶段
用户发起提问后,流程完全反过来。
第一步,RAGFlow 判断这个问题需要走检索流程,把用户问题和会话 ID 写入 Redis 作为请求上下文。
第二步,系统读取知识库配置里的 embedding 模型信息,把用户的问题文本转成查询向量。这个向量会带着知识库 ID 去 ES 里做检索。
第三步,ES 执行向量检索和全文检索的混合查询,合并结果后做重排,返回 topK 个 chunk 的 ID 列表和对应文本。这一步也是检索层出结果的一步,整个查询通常控制在 100 毫秒以内。
第四步,后端拿着 chunk ID 列表去 MySQL 查询关联的文档信息和 chunk 元数据。比如"这个 chunk 来自《2024年度采购合同.pdf》第 3 页",这些信息用于拼装回答的引用来源。
第五步,系统把所有召回内容和元数据一起组装成 Prompt,发送给大模型。大模型生成回答后,引用来源从 MinIO 获取原始文件预览。最终用户看到的效果是:回答正文 + 引用列表 + 可点击的原文预览。
从这条链路上可以看出来,一个简单的问答背后,四层存储全部参与了一遍。只要任何一层出现延迟或数据异常,用户都会直接感受到——要么回答慢了,要么引用的原文打不开,要么检索结果不相关。
7. 常见问题与排查技巧实录
最后分享一些我实际运维 RAGFlow 过程中的排查经验和技巧。这些问题不一定每个都会遇到,但遇到了之后知道从哪里下手,能省很多时间。
7.1 高频问题排查速查表
| 问题现象 | 排查思路 | 解决方向 |
|---|---|---|
| 文件上传成功但解析一直卡住 | 检查任务消息队列,看 worker 是否有报错 | 查看容器日志,定位解析报错 |
| 解析成功但检索不到内容 | 确认 ES 索引是否写入成功,检查 knowledge base 过滤条件 | 对比 MySQL 任务状态和 ES 文档数量 |
| 问答回答慢 | 查看 ES 查询耗时,确认向量检索和重排都参与了 | 调整 ES 分片数量、增加硬件配置 |
| 引用来源加载失败 | 确认 MinIO 中的文件是否还存在,检查预签名 URL 有效期 | 检查 MinIO Bucket 配置和凭证有效性 |
| 删除知识库后仍能搜到内容 | 确认 ES 索引文档是否同步删除 | 清理 ES 中的残留索引 |
| 切换 embedding 模型后检索异常 | 检查向量维度是否匹配 | 删除旧索引并重建知识库索引 |
| 容器磁盘空间被撑满 | 查看 MinIO 数据目录大小和 ES 索引大小 | 清理不需要的知识库,归档旧文档 |
7.2 docker 部署与数据目录规划
RAGFlow 的 docker 部署看起来简单,跑起来也简单,但如果你想长期稳定运行,有几个数据目录规划的细节值得注意。
第一,宿主机上为每个存储组件规划独立挂载点。不要把所有数据卷放在同一个磁盘分区。MySQL、MinIO 和 ES 的 IO 特征完全不同:MySQL 偏随机读写,MinIO 偏顺序读写,ES 的读写压力更大。放在不同磁盘上能有效避免 IO 争抢。SSD 优先给 ES 和 MySQL,机械盘可以用来放 MinIO 的大文件。
第二,ES 的内存设置是性能瓶颈的关键。ES 的 JVM 堆内存默认值可能不适合你的机器配置。堆内存太小会导致频繁 GC,检索延迟飙升。一般来说,给 ES 分配不超过物理内存 50% 的堆内存,留足够空间给操作系统做文件缓存,能获得更好的检索性能。
第三,批量处理文件时控制并发数。RAGFlow 支持批量上传和解析,但一次性塞几百个文件进去,任务队列会瞬间打满,Worker 节点内存可能直接耗尽。我在生产环境里的做法是分批上传,每批控制在 50 个以内,根据机器配置调整。
第四,定期做数据清理和备份。MySQL 可以做逻辑备份,MinIO 可以做桶级别的同步备份,ES 可以做索引快照。不要等磁盘满了再去处理,RAGFlow 的存储组件之间数据强关联,清理某一个组件的数据常常需要联动其他组件,临时处理容易出问题。
7.3 个人实操中的一点体会
用 RAGFlow 一年多,我最深的感触是:这套四层存储架构虽然看起来复杂,但它解决的是真实问题。单机系统用单体数据库可能能跑,一旦数据量上来、并发上来、文档复杂度上来,分层带来的隔离性和弹性优势会越来越明显。
如果你要基于 RAGFlow 做二次开发,我建议优先研究 MySQL 里各表之间的关系,再对比 ES 索引结构,最后才去碰对象存储和缓存。因为整个系统的核心逻辑是"关系数据驱动一切":文件、任务、索引、会话,最终都汇聚到元数据层的记录上。把这条主线的数据模型吃透了,后面所有扩展和问题排查都会轻松很多。
四层存储不是一种炫技,而是一种务实的选择。每一层都有它不可替代的理由,理解这些理由,比死记架构图有用得多。