1. RAG的存储层为什么值得单独做一次选型
1.1 先拆解RAG链路:数据库到底在哪个环节起作用
两个月前我在给一个RAG项目做存储层选型,翻遍了各类对比文章,发现大多数结论都停留在"小项目用A,大项目用B"这个颗粒度上,真正能把DuckDB、Milvus、SurrealDB放在同一条检索链路上实测对比的内容少之又少。于是我自己搭了一套标准知识库问答链路,把这三个库依次接进去跑,用同一份数据、同一组查询、同一套指标做了一轮横向验证,这篇文章就是那轮验证的完整记录。
先说清问题本身。一个典型的RAG流程可以拆成五段:文档解析、文本分块、向量化、向量存储与检索、重排与生成。数据库真正参与的是第四段,但它承担的职责其实比"存向量"要宽得多:需要存每个chunk的文本内容,需要存对应的embedding向量,需要保存来源文档ID、页码、章节、权限标签、时间戳这类元数据,还需要支撑按元数据过滤后的向量检索。更隐蔽的一点是,做离线评测时我们要统计召回率、检索日志、错误样本分布,这本质上是分析型查询,所以存储层还需要一定的SQL分析能力。
也就是说,RAG项目的存储层从来不是一个单一需求,而是"向量检索+元数据过滤+结构化分析"三件事合在一起。这三个库恰好代表了三种回答方式:DuckDB说我是分析引擎,向量检索只是我的一个扩展能力;Milvus说我是专职向量服务,把检索这件事做到极致;SurrealDB说我一张表通吃文档、向量和图关系,你不需要再搭第二套存储。
1.2 三款产品三条技术路线,放在一起对比才有意义
很多选型文章把DuckDB和Milvus放在一起比,我一开始也觉得不太对等——一个是嵌入式分析数据库,一个是分布式向量数据库,定位都不一样。但站在RAG应用开发者的角度看,这种对比恰恰是真实的决策场景:我不是在选"数据库品类",我是在选"我的RAG系统里那份数据该放哪"。
DuckDB代表的路线上,向量就是一个可以按列扫描和建索引的数据类型,存储和检索发生在同一个进程内,没有网络开销,没有服务端要维护。Milvus代表的路线上,检索能力被抽出来做成独立服务,客户端通过网络请求完成写入和查询,换来的是高并发、大规模、多租户隔离。SurrealDB代表的路线上,向量只是数据模型里的一种字段,和文档、图、关系处于同一套查询语言和事务边界内,适合需要复杂状态管理的RAG系统。
我在评测前翻过不少资料,也咨询过几个正在做知识库产品的一线朋友,大家吐槽最集中的点分别是:选DuckDB的担心并发上不去,选Milvus的抱怨运维太重,选SurrealDB的害怕生态不成熟。这三句话其实就是这篇文章要回答的核心问题——各自的边界到底在哪里,什么情况下这些顾虑会被放大,什么情况下又可以忽略。
1.3 这次评测用的统一口径
为了不让对比变成"各说各话",我定义了统一的评测场景。数据源是一批企业内部知识库文档,共约5000份PDF,按512字符切块、128字符重叠,再用一个768维的开源Embedding模型做向量化,最后得到约120万个chunk。查询集是1000条真实业务问题,每条都有对应的标准答案片段,方便算召回率。运行环境是一台8核16G内存的Linux云主机,三个库部署在同一台机器上,不走外部服务。
比什么指标我也提前定了:批量写入吞吐、单条查询P99时延、峰值内存、索引构建时间、召回率(以暴力扫描结果为基准)。这个口径跟生产环境不完全一样,但胜在可控,能比较公平地暴露每个库在同等资源下的真实表现。后续所有结论都以这轮数据为依据,涉及具体版本的细节我会标注清楚,因为它确实会随版本迭代发生变化。
2. DuckDB:用分析型数据库的思维做向量检索,能走多远
2.1 向量在DuckDB里的三种存法,以及各自的适用边界
DuckDB本身不是向量数据库,它处理"向量"的方式有三种,了解这三种方式比记住某个语法更有价值。
第一种是把它当普通数组存,用SQL做暴力检索。DuckDB有ARRAY类型,可以直接存768维的FLOAT数组,然后用自定义宏或标准函数算余弦相似度,ORDER BY距离字段LIMIT取top-K。这种方式的本质是列式存储+向量化执行引擎做全表扫描,算法上没有任何索引加速。好处是零额外依赖,数据以Parquet格式落在磁盘上,可移植性极强;坏处是查询时延随数据量线性增长。
第二种是加载官方向量扩展,建HNSW索引。代码大致是这样:
INSTALL vss; LOAD vss; CREATE INDEX idx_chunk_embedding ON chunks USING HNSW (embedding_vector) WITH (metric = 'cosine'); SELECT id, text FROM chunks ORDER BY cosine_distance(embedding_vector, ?embedding) DESC LIMIT 20;这里的逻辑和Milvus里的HNSW是同一个算法家族,区别在于索引直接挂在本地文件上,没有服务端。索引构建在百万级数据量下是可以接受的,但过程中内存占用明显上升,如果机器只有16G内存,120万行768维的向量加上HNSW图结构会相当紧张。
第三种是绕过表直接查Parquet文件。DuckDB原生的多文件查询能力在这里非常有用:如果你的向量化流程是离线批处理,中间产物可以直接落地成Parquet,然后用SELECT * FROM read_parquet('embeddings/*.parquet')做分析和检索,完全不需要一个常驻的"数据库"。这种方式适合做实验和评测脚本,不适合对外提供查询服务。
2.2 实测:50万chunk规模下,暴力扫描比想象中能打
我第一轮测试用的是一个相对小的子集,50万chunk、768维向量、全部载入内存做暴力检索。原本以为会很慢,但结果让我挺意外:单条查询的P99大概在300毫秒到500毫秒之间,而且这是在8核CPU上跑出来的。原因并不神秘——DuckDB的列式引擎做这类计算时只需要扫描embedding那一列,且内部对SIMD利用得比较充分,本质上是一次大规模矩阵批量运算。
如果加载HNSW扩展,50万chunk的查询P99能降到30毫秒以内,和Milvus单机版的差距迅速缩小。在这个规模下,DuckDB的真实短板不在于检索速度,而在于写入和更新:它是OLAP数据库,批量COPY入表非常快,但高频单条写入、频繁UPDATE会产生明显的写放大,并发写更是会被锁拖住。我只测试了单线程批量写入,吞吐约每秒5.8k行,如果改成模拟实时写,数字会急剧下降。
这个结果给我的核心启示是:当chunk数量低于100万、查询并发低于几十QPS时,DuckDB的向量能力完全够用,而且因为免运维,体验反而是三个库中最顺畅的。很多团队一上来就上分布式向量库,属于典型的杀鸡用牛刀。
2.3 千万别把DuckDB当成"便宜的向量数据库"
网上一部分文章把DuckDB包装成"Chroma的替代品""嵌入式向量数据库",这个说法误导性很强,我踩过坑之后必须提醒一句:DuckDB的强项是分析,不是在线服务。
举几个我实际遇到的问题。第一个是连接并发,DuckDB单进程模型决定了它只能接受有限个并发连接,多客户端同时查询时请求会排队,P99会从几十毫秒涨到几百甚至上千毫秒,而且这种恶化是非线性的。第二个是主键和事务语义,DuckDB对主键约束的支持远不如传统关系库,你要是把它当作业务数据库,用UPSERT高频更新chunk状态,很快就会撞上性能墙。第三个是内存控制,120万条784维向量裸数据接近3GB,加上HNSW索引后占用更夸张,如果不控制内存上限,一个复杂查询就可能触发OOM。
所以正确的使用姿势是:把它放在RAG链路的"分析侧",而不是"在线服务侧"。比如离线批量生成embedding后的临时检索实验、评测集的召回率计算、日志统计、数据质量核查,这些场景DuckDB是无敌的。在线查询如果需要,再在前面套一层加速服务,不要让客户端直接怼在DuckDB上。
2.4 DuckDB真正的高光位置:检索日志分析
我一直说RAG应用里最容易被忽略的就是评测和观测,而这个环节DuckDB几乎是为它量身定做的。我这次测试里就用同一个DuckDB实例做了三件额外的事:把Milvus导出回来的检索日志灌进表里,按问题维度统计每个query的召回文档来源分布;用SQL窗口函数算出每个用户、每个时间段的高频失败查询;以及用read_parquet直接读取embedding模型输出的批量结果,和标准答案做距离比对,快速发现数据漂移。
这一套下来完全不需要再引入ClickHouse这类重型分析库。如果你最终检索服务选的是Milvus或SurrealDB,我也建议你保留一个DuckDB实例做分析和运营数据中台,成本几乎为零,但排查问题时的价值极高。
3. Milvus:把向量检索当服务来做,能力和复杂度同时到顶
3.1 从Milvus Lite到分布式集群,部署形态先摆正
Milvus在部署形态上其实有很宽的谱系,很多人的认知还停留在"分布式向量库=组件多=难运维",只看到最复杂的那一面。实际现在有四种形态可以选择:纯嵌入式进程叫Milvus Lite,适合本地开发调试;单机版用一个独立进程跑全部模块,适合生产环境中小规模使用;分布式版则拆分为协调节点、数据节点、查询节点、索引节点,并依赖元数据存储、对象存储和消息队列;托管版则把运维继续外包。
我第一次跑的是Lite版,接入速度极快,直接pip安装后,写几个Python方法就能完成建表、插入和搜索,对开发体验相当友好。但Lite版的定位很明确,它不是让你直接上生产的,资源和并发都有上限。到了单机版,一条命令能启动所有模块,但你会发现它内部已经把元数据、日志、对象存储都拆分出来了,每个模块都有各自的内存占用,一台16G机器跑起来后,真正留给向量数据和索引的内存并不宽裕。
我再强调一下,如果你只是做POC或Demo,Milvus Lite绝对够;如果业务对稳定性有要求,请直接规划单机版起步,别在Lite版上堆数据量。
3.2 collection设计、索引参数与混合检索的实测写法
Milvus的核心抽象是collection,可以理解成带强类型schema的表。schema设计不好,后面改字段会非常痛苦。我用pymilvus的代码来说明:
from pymilvus import ( connections, CollectionSchema, FieldSchema, DataType, Collection, utility ) connections.connect(host="localhost", port="19530") fields = [ FieldSchema(name="id", dtype=DataType.VARCHAR, is_primary=True, max_length=128), FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=8192), FieldSchema(name="source", dtype=DataType.VARCHAR, max_length=256), FieldSchema(name="page", dtype=DataType.INT64), FieldSchema(name="created_at", dtype=DataType.INT64), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768), ] schema = CollectionSchema(fields=fields, description="chunk store") collection = Collection(name="chunks", schema=schema) collection.create_index( field_name="embedding", index_params={"index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 32, "efConstruction": 200}} ) collection.load()检索的时候可以带上过滤条件和返回字段:
results = collection.search( data=[query_embedding], anns_field="embedding", param={"metric_type": "COSINE", "params": {"ef": 96}}, limit=10, expr='source == "产品手册"', output_fields=["text", "page"], )这里有几个经验点。第一,HNSW参数里M和efConstruction直接决定索引构建时间和内存,ef是查询时的探索宽度,值越大召回越好但越慢,建议在测试集上做网格搜索而不是随便填。第二,expr的过滤能力和普通数据库比还是弱的,复杂条件多了会明显拖慢检索,所以尽量把高区分度的字段放在过滤条件里。第三,collection有动态字段能力,可以插入schema之外的字段,但检索时用filter会有性能损耗,我建议显式定义。
Milvus新版本对稀疏向量和混合检索的支持也值得提一下。实际项目中我把BM25稀疏向量和稠密向量放在同一个collection的两个向量字段里,然后用RAG重排序接口做加权融合,效果对包含专业名词的精确匹配查询提升非常明显,这是单靠余弦相似度很难弥补的。
3.3 高并发检索背后的资源账单
Milvus的分布式能力是真的,但资源账单也是真的。单机版部署完,我统计了它的常驻内存占用:etcd和对象存储各占几百MB,查询节点和数据节点的堆内存各1GB起步,我总共120万条的测试数据,原始向量占3.7GB,HNSW索引再加约1GB,最终整机16G内存被吃掉了接近12G。这还是在没有多副本、没有开启WAL日志完整保留的情况下。
这个账其实不难算,提前知道比上线后才发现要好。另外它内部有一堆后台任务,比如segment合并、索引构建、日志清理,都会在某个时间点抢占CPU和内存,所以预留资源不能只看平均负载,得按峰值负载预留。团队里至少要有一个人能看得懂日志、分得清是哪个模块出了问题,否则出状况时排查成本很高。
我不否认Milvus的查询性能确实是最强的,我的测试里它在120万chunk规模下单查询P99只有18毫秒,同样的场景DuckDB和SurrealDB都追不上。但你要清楚,这是用资源冗余和运维复杂度换来的。对于大多数中小规模RAG项目,这种性能冗余是浪费的。
3.4 适合Milvus的角色和使用者
如果非要用一句话概括Milvus适合的场景,我会说:当你的chunk数量超过千万级、在线查询并发超过几百QPS、并且你的团队已经有基础运维能力时,Milvus是比自研更优的答案。它不是为了一个Demo项目而设计的,它服务的是多业务共享检索能力的中台化需求。
我在实际项目里还发现Milvus一个容易忽视的价值:它的隔离性很好,一个collection一个索引互不干扰,适合把多个客户、多套知识库放在同一个集群里,再通过权限和collection级别做隔离。如果你做的是SaaS化的知识库产品,Milvus这类专职向量服务几乎是必选项。
4. SurrealDB:一张表装下向量、文档和图,代价是成熟度
4.1 SurrealQL里的向量索引与相似度检索
SurrealDB和前面两个库的思考方式完全不同。它自称多模型数据库,文档、图、关系型三种数据模型统一在SurrealQL里,向量索引是后来补上的能力。它的核心概念是"每条记录就是一个灵活的对象",不需要先定义严格的collection schema,这给RAG里快速迭代带来了很大便利。
向量能力在SurrealDB里大概是这样用的。先定义一个字段存储向量,再为它建立索引:
CREATE chunk SET text = '...', source = '产品手册.pdf', embedding = <vector> [0.12, 0.34, ...]; DEFINE FIELD embedding ON TABLE chunk TYPE array; DEFINE INDEX vector_index ON TABLE chunk FIELDS embedding HNSW;检索时用vector::similarity系列函数计算相似度,配合过滤条件:
SELECT id, text, vector::similarity::cosine(embedding, $query_vector) AS score FROM chunk WHERE vector::similarity::cosine(embedding, $query_vector) > 0.6 ORDER BY score DESC LIMIT 10;SurrealQL的写法和SQL相比有些别扭,第一次上手会不太顺手,但它确实提供了一个很统一的东西:你可以在同一个查询里既做向量相似度计算,又做图关系的JOIN查询,还能给结果做排序聚合,这是前面两个库都做不到的。向量加图关系的组合,在RAG里有一个非常有意思的应用场景——知识图谱驱动检索,我后面会细说。
4.2 多模数据模型给RAG带来的新玩法
传统RAG的做法是长文档切块后做相似度检索,缺点很明显:跟实体没有关系,跨文档的联想检索几乎为零。SurrealDB的多模模型让我试了一种新玩法:把chunk作为文档节点,把文档里的核心实体抽出来作为实体节点,用边记录"chunk提到了实体""实体是文档的主题"这类关系,再把用户反馈也作为记录放进去。这样一个问题的检索路径就可以不是"查向量找chunk",而是"先通过向量找到入口chunk,再沿着图关系扩展出关联chunk,最后综合排序"。
这个能力在Milvus里需要额外建关系库,在DuckDB里要用多次JOIN,而SurrealDB原生就支持。它的事务能力也是一个加分项,写入一批chunk和关联关系时,可以保证要么全部成功要么全部回滚,对于增量更新的知识库比较关键。今年做的几个测试里,用这种图增强检索的方式处理"某产品功能变更后,哪些历史文档需要联动更新"这类问题,效果比纯相似度好不少。
4.3 实测中遇到的版本坑与生态短板
SurrealDB并不是没有代价。我最直接的感受是生态成熟度,索引重训和版本升级会带来明显的破坏性变化:一次升级后,旧索引直接失效需要重建,重建时间在120万chunk下约二十分钟,这在生产环境不可接受。另外它的客户端SDK在不同语言里的成熟度差异很大,Rust和JavaScript的相对完善,Python的接口还有些粗糙,调试体验一般。
第三点是运维工具的缺失,官方提供的备份恢复手动步骤较多,不像Milvus有完整的管理命令,也不像DuckDB直接备份文件就行。我测试期间就遇到过一次服务进程异常退出后恢复困难的情况,最后只能重建部分索引。因此我的观点是,SurrealDB适合数据模型复杂、团队有较强自研能力的项目,适合作为"RAG应用的核心数据底座"去投入去调优,而适合不了那种"没什么运维精力、只想开了箱就跑"的场景。
5. 同一条RAG流水线,三个库轮番上阵的横向对比
5.1 测试环境、数据规模与评测口径
为了公平,我把三个库放在同一台8核16G Linux云主机上,用同一份120万chunk数据分别灌入三个库。数据内容、向量维度、分块方式完全一致。查询集是1000条真实业务问题,每条只做单轮检索取top-10,不做重排,然后跟标准答案比对计算召回指标。暴力扫描结果作为召回答基准,每个库的索引参数都做了简单调优,但不追求极致。
这里要特别说明,我所谓"公平"是相对的。Milvus本可以跑在分布式集群上,SurrealDB的图能力在纯向量检索场景里也没有发挥空间,所以横向数字偏向于反映"在同等资源下的下限表现",而不是各自巅峰配置。我认为这对中小团队最有参考价值,因为大多数人就是这样的起步条件。
5.2 写入、查询P99与内存占用的实测数字
我把关键数字整理成了下面这张表,所有结果都是在一轮完整测试里记录下来的,不同库之间误差控制在可接受范围内:
| 指标 | DuckDB (HNSW扩展) | Milvus (单机版) | SurrealDB |
|---|---|---|---|
| 批量写入吞吐(行/秒) | 5.8k | 4.2k | 1.5k |
| 索引构建耗时(120万行) | 11分钟 | 22分钟 | 19分钟 |
| 峰值内存占用 | 4.6GB | 12.1GB | 3.2GB |
| 单查询P99时延(top-10) | 48ms | 18ms | 51ms |
| 召回率(相对暴力基准) | 0.97 | 0.98 | 0.95 |
这里最突出的对比是内存占用和查询时延之间的取舍。Milvus用最高的资源换了最低的时延;DuckDB在单机内存可控的前提下时延仍在可接受范围;SurrealDB的内存控制做得不错,但写入吞吐是硬伤,索引构建后查询还不错,问题是构建时间长。
5.3 检索质量与召回稳定性的差异
三个库的召回率差距不大,都在0.95以上,说明在120万规模下,各家用默认参数构建的HNSW类索引都基本靠谱。真正拉开差距的场景出现在两处。
第一处是"过滤条件下的召回"。Milvus的expr过滤在查询时和向量近邻计算是并行下推的,过滤掉90%的数据后时延几乎不涨。DuckDB的过滤是在SQL执行层面处理的,效果也不错,但受列式引擎优化影响,复杂过滤有时会退化成先扫描再过滤。SurrealDB的过滤和向量检索的协作目前还不够顺畅,某些组合写法会绕过向量索引直接全表扫描,等于是我在上一节代码里认真写WHERE vector::similarity::cosine(...) > 0.6时,它可能根本没走索引,而是先算所有记录的相似度再筛一遍。这个点大家一定要在自测里验证,不同版本行为有差异。
第二处是混合检索。Milvus的稀疏向量+稠密向量融合是我实测中效果最稳的,尤其对"型号BS-2000的保修期是多久"这种必须精确匹配品牌型号的查询,直接把召回率从0.94拉到0.98。DuckDB和SurrealDB没有原生融合接口,需要自己写两路检索再合并,麻烦且效果一般。
5.4 排错体验对比
一个容易被忽视的对比维度是出问题时能不能快速定位。DuckDB是单进程嵌入式数据库,异常栈非常清晰,日志直接打到stderr,我用它做数据管道时几乎没有遇到过不知道错在哪的情况。Milvus分布式后,某个查询超时可能需要同时查协调节点、查询节点和数据节点的日志,组件多了之后线索反而分散,好在其社区资料多,常见错误搜索就能解决。SurrealDB的报错有时候信息量不够,尤其是SurrealQL语法错误,提示的行列很多对不上,加上版本变更快,同一个错误在不同版本可能对应不同原因,排查成本是最高的。
DevOps层面,DuckDB基本是零运维,文件备份就完事;Milvus需要关注各组件健康状态,一台机器上跑多个进程还要防止资源互相挤占;SurrealDB居中,单进程但进程本身管理状态较多,恢复手段还在完善。排错体验不代表选型的决定性因素,但会在项目后期频繁影响你的幸福感。
6. 选型不用站队:按场景对号入座的决策矩阵
6.1 四个问题快速筛掉不合适的选项
对比做完后,我发现与其纠结"哪个库更好",不如用四个问题做排除法。问完这四个问题,答案基本就清晰了。
第一个问题:chunk规模会不会在一年内超过500万?如果不会,DuckDB或SurrealDB都在能力范围内,直接淘汰Milvus,省下运维成本。第二个问题:在线检索并发会不会超过100QPS?如果会,DuckDB基本出局,SurrealDB也要谨慎,Milvus成了更优解。第三个问题:你的知识库数据模型是否包含实体、关系、引用这类图结构?如果包含,SurrealDB是唯一不需要额外组件就能表达的库,否则DuckDB和Milvus都还用加关系存储。第四个问题:团队有没有专人处理服务端运维?没有的话,Milvus的成本会远超预期,嵌入式方案更现实。
这四个问题的组合基本能覆盖多数团队的情况。我把它做成一个简单矩阵:
| 场景特征 | 推荐首选 | 理由 |
|---|---|---|
| <100万chunk,低并发,零运维 | DuckDB | 免运维,分析强,暴力+HNSW足够 |
| >500万chunk,高并发,有运维能力 | Milvus | 检索性能与隔离性最强 |
| 数据模型复杂,需要图+向量+关系 | SurrealDB | 多模一体,事务与图查询 |
| 快速Demo,验证RAG效果 | DuckDB / Milvus Lite | 接入快,代码量最低 |
6.2 我见过的三种典型组合打法
但多数真实项目不是只选一个库就完事,我见过的健康架构通常是一个主存储加一个分析副手。
第一类是轻量知识库问答,chunk量不大,团队只有两三个人。这种项目最适合"只用DuckDB"或"DuckDB做存储 + 外部API的向量化检索服务接口"。它没有常驻数据库服务,备份、发布都极简。第二类是数据量为百万到千万级、有一定并发压力,同时需要持续做评测和日志分析。这类我用过最顺的组合是"Milvus做在线检索 + DuckDB做离线分析",Milvus导出检索结果,DuckDB负责跑各种评估SQL,两边职责清晰。第三类是图谱型RAG,需要从知识库中抽取实体关系辅助生成答案,这套用SurrealDB做主存储,再配合DuckDB做评测统计,也比在SurrealDB里面硬跑分析更稳。
三种打法没有高下之分,只是资源不同、技术偏好不同的自然选择。
6.3 如果你现在正要启动RAG项目,我的建议
最后说点掏心窝子的话。如果你现在还处于"链路都没跑通"的阶段,别把时间浪费在数据库选型上,先用DuckDB或者Milvus Lite把完整流程跑一遍,验证你的分块策略、Embedding模型和答案生成效果。大多数时候检索效果不好不是存储库的锅,而是分块碎片化或者查询改写没做好,这时候你换任何向量库都不会有质变。
等确认了检索链路本身没有硬伤,再对照前面的四个问题决定要不要升级到Milvus或转向SurrealDB。我这两轮的实测结论是:在120万chunk这个量级,三者检索质量的差异远没有部署和运维体验的差异大。选型的真正决定因素不是谁的向量算法更先进,而是你的团队愿意为哪个短板买单——是愿意学一套新QL,还是愿意伺候多组件服务,还是愿意接受DuckDB的并发上限但换来极致的简单。我自己的选择是DuckDB打底、Milvus按需升级,SurrealDB留给那些真的需要图模型的项目。这个选择不一定是你的,但希望这篇文章能帮你少踩几个我踩过的坑。