RobustMQ 支持向量检索和全文检索:一些思考
起点
最近在做 mq9,遇到一个绕不开的问题——Agent 注册中心要支持语义检索。
A2A 协议里 Agent 用自然语言描述能力(AgentCard 的 description、skills 字段都是自然语言)。客户端找 Agent 时也是自然语言("我想找一个会写 Rust 的 Agent")。两边都是自然语言,中间不能只靠 tag 精确匹配,必须有向量匹配。
这个需求把"向量检索"这件事提到了 RobustMQ 这边。
但仔细想,问题不只是 mq9 注册中心。RobustMQ 的设计原则一直是"协议中立 + 不解析消息内容"——broker 不知道消息是什么格式,没有内置任何检索能力。如果用户想对消息做检索,自己接 Elasticsearch、Qdrant 或别的工具。
那是不是借这个机会,把检索能力作为 RobustMQ 的一个组件沉淀下来?
这件事翻来覆去想了几轮,每轮的判断都在演进。这篇文章把过程梳理一下。
第一轮判断:要不要做
最早的判断是不做。理由很直接——基础设施项目最怕 scope 蔓延。已经有 Elasticsearch、Qdrant 这些成熟方案,RobustMQ 重复造轮子没意义。把消息中间件的核心能力做扎实,检索让用户自己选。
这个判断在 mq9 出现之前是对的。但 mq9 注册中心的需求改变了局面。
Agent 注册中心必须有向量检索。Agent 用自然语言描述能力、客户端用自然语言查询,中间没有语义匹配根本走不通。这不是可有可无的增强,是 mq9 这件事必须解决的问题。
为了它,RobustMQ 必须引入向量检索的技术栈。这套技术栈一旦引入,就是 RobustMQ 的一部分。
那问题就从"要不要做"变成"既然要做,怎么做最合理"。
第二轮判断:放在哪里
技术栈必须引入。下一个问题是放在哪里。
最直接的方案是放在 mq9 模块里——LanceDB + fastembed 是 mq9 注册中心的实现细节,其他模块不感知。
但这个方案有问题。
第一,技术栈一次引入应该多次复用。引入 LanceDB 和 fastembed 的工程成本是真的——熟悉 API、处理边界情况、维护版本、写文档。这个成本付了一次,就应该让需要的地方都能用,不是只服务一个模块。
第二,类似的需求会反复出现。除了 Agent 注册中心,topic 内容检索、Connector 内容路由、Bridge 内容过滤、未来其他场景都可能需要按内容检索。如果检索能力绑死在 mq9 里,每个场景都要重新引入一遍。
第三,混在 mq9 里会让 mq9 模块更复杂。mq9 既要管协议,又要管检索,逻辑混在一起。
正确的方向是把检索抽出来作为 RobustMQ 内部一个独立的模块。
第三轮判断:怎么抽象
抽出来作为独立模块是对的。但抽象怎么做?
一个常见的陷阱是过度抽象——为了"将来能给别人用",加一堆扩展点、配置项、插件机制。结果模块本身做不好,反而连第一个场景都服务不好。
正确的思路反过来——把当前需求做好,干净的抽象自然就有了。
具体的:
- 接口就是 save / search / delete 三个基本操作
- 用 namespace 隔离不同调用方的数据
- 内部用 LanceDB + fastembed,但 trait 抽象保留替换空间
- 不预设其他场景会怎么用
这种抽象一旦做好,长期看自然能服务多个场景——不是因为我们追求"通用",是因为干净的抽象本来就和具体场景解耦。
通用性是好抽象的结果,不是设计目标。这个区别很重要。
设计原点:服务 Agent Discover
把这件事的优先级重新理一下。
Agent Discover 是当前阶段必须做的事。没有它 mq9 走不通。它的特点是:
- 数据量小(几万到几十万条 AgentCard)
- 写入低频(Agent 注册不频繁)
- 查询频率中等(DISCOVER 调用不会每秒几千次)
- 没有明显的性能或成本压力
所有设计决策围绕这个目标做。能服务 Agent Discover 的设计就是好设计,超出 Agent Discover 需求的复杂度都是过度设计。
topic 内容检索是另一个真实的应用场景,但工程节奏上不是当前阶段的事。它的挑战和 Agent Discover 完全不在一个量级——百万级数据量、高频写入、embedding 推理成本可观、存储成本翻倍。这些挑战要解决,但等 Agent Discover 跑通后再处理。
短期聚焦 Agent Discover,长期能服务 topic 检索——这两件事不矛盾。关键是抽象做对、实现做好。
关键的设计判断:不解析消息
这件事最重要的设计判断单独拿出来讲。
RobustMQ 一直坚持的核心原则之一是"broker 不解析消息内容"——消息体是 byte 数组,broker 不知道它是 JSON、Protobuf 还是别的。这是 RobustMQ 协议中立的根本。
加了检索能力后,要不要打破这个原则?
一种方案是打破——让用户在 topic 配置里声明"消息是 JSON,里面 $.payload.text 字段要建索引"。RobustMQ 按 schema 解析消息提取字段,分别建索引。
但这个方案有问题:假设消息是 JSON、用户要维护 schema、broker 多了"半解析"逻辑、不同消息格式要写不同适配器。
另一种方案是不打破——直接对消息内容整体做索引:
- 全文索引:拿到 payload 字节,用 tokenizer 拆词建 BM25 倒排索引
- 向量索引:把 payload 当作一段文本,送给 embedding 模型生成向量
RobustMQ 不知道 payload 里有什么字段,也不需要知道。是 JSON 也好、Protobuf 也好、纯文本也好,一视同仁。
这个方案的代价是精度——结构化字段的精确语义被淹没在整体文本里。但代价是可接受的:
- 全文检索本来就是模糊匹配,关键词出现就能命中
- 向量检索找的是语义相似,整体文本的语义大致对得上
- 真要按字段精确匹配("找 status=created 的消息"),那是 SQL 或业务逻辑的事,不是 SearchEngine 的目标
对基础设施层来说,通用性比精度更重要。这个判断决定了 SearchEngine 接口的简洁——不需要 schema 概念、不需要字段路径、不需要类型推断。调用方传"待索引内容 + 元数据",SearchEngine 处理。
这个判断也让 RobustMQ 协议中立的核心原则保持不变。检索能力是叠加在原则之上的,不是对原则的修改。
能力范围:四种基本检索
把"够用"具体化成四种检索能力:
按 key 检索:精确匹配某个标识
按 tag 检索:精确匹配某个标签或标签集合
全文检索:基于 BM25 的拆词倒排索引
向量检索:基于 embedding 的语义匹配四种可以组合(先按 tag 过滤再做向量检索)。但不做超出这四种的能力:
- 不做复杂查询语法(嵌套 bool、function score 等)
- 不做聚合分析(group by、统计、histogram)
- 不做高级评分(reranking、cross-encoder、自定义 scoring)
- 不做大规模索引(千万级以上数据量)
这些需求请使用 Elasticsearch、Solr 等专业搜索引擎。
四种基本能力覆盖了消息系统里大多数检索场景。把它们做扎实就够,不试图做更多。
这个范围长期不变。即使 SearchEngine 演进,能力扩展也只在四种基本能力内深化(比如支持更多 tokenizer、向量索引参数调优、tag 检索支持嵌套结构等),不增加新的能力类型。
技术选型
具体的技术选型不长说,几个关键决策:
LanceDB 作为向量存储和 FTS 引擎。
LanceDB 是嵌入式向量数据库,pure Rust 实现。Cargo 依赖一行就能用,数据存本地目录,不需要外部进程。这和 RobustMQ"单二进制、零外部依赖"的整体哲学一致。
LanceDB 同时支持向量检索和全文检索(基于 Tantivy,BM25 评分),一个组件覆盖两种能力。不选 Qdrant、Weaviate、Milvus 的理由是它们都是 server-based,需要部署独立服务,增加运维复杂度。
fastembed-rs 作为默认 embedding 模型。
纯 Rust 的 embedding 库,内置主流开源模型(BGE、Jina、E5),基于 ONNX Runtime 做 CPU 推理。不调用任何外部 API。
默认用 BGE-small(130MB,384 维)。允许配置覆盖(用更大的模型或者外部 embedding 服务)。
默认实现追求"够用",不追求最优。
embedding 模型选小的(BGE-small 而不是 BGE-M3)。索引选简单的(数据量小不建索引,超过阈值才建 IVF_FLAT)。不做复杂的查询优化和缓存策略。
理由是 SearchEngine 服务的是 RobustMQ 内部的常规检索需求,不是专业搜索引擎的高强度场景。够用就好。如果有人需要更高的精度或更大的规模,通过配置覆盖默认实现,或者外接专业组件。
索引失败不影响主存储写入。
embedding 推理可能失败(模型加载失败、内存不够等)。索引写入可能失败(LanceDB 故障等)。
原则:索引失败是次要错误,记日志、可能重试,但消息本身的写入不能因此回滚。这个原则保证 RobustMQ 的核心能力(消息可靠存储)不被增强能力(检索)拖累。
写入索引默认异步。
FTS 索引快(拆词 + 倒排索引,毫秒级)。向量索引慢(embedding 推理 30-150ms)。同步建索引会让消息写入延迟翻几倍。
默认异步——消息先写主存储立即返回,索引在后台慢慢建。代价是有"刚 save 的还查不到"的窗口(几秒到几十秒)。需要严格实时检索的场景可以配置同步模式。
SearchEngine 在 RobustMQ 里的位置
把这个能力放回 RobustMQ 整体架构里看:
RobustMQ
├─ Protocol Layer(MQTT, Kafka, AMQP, NATS, mq9)
├─ Meta Service(管理元数据,包括 mq9 注册中心)
├─ Storage Layer(File Segment, RocksDB, Memory)
└─ Search Engine(新增的能力层)SearchEngine 是独立模块,被 Meta Service、Storage Layer 等内部模块调用。它不属于协议层(不绑定具体协议),不属于存储层(不只服务消息存储),是和它们平级的能力层。
外部用户不直接接触 SearchEngine。他们感知到的是:
- mq9 的 AGENT.DISCOVER 能做语义检索
- 某个 topic 配置开启检索后,可以通过 search API 查询消息
SearchEngine 的存在让这些能力变得自然,但用户不需要直接面对它。
第一批使用场景
mq9 注册中心。 Meta Service 管理 AgentCard 元数据。AgentCard 注册进来时,Meta Service 把每个 skill 的相关字段(name、description、examples、tags)拼接成一段文本,调用 SearchEngine.save 传入这段文本和元数据(agent_id、mailbox、raw_card 等)。客户端 DISCOVER 时,Meta Service 调用 SearchEngine.search 拿到匹配的 record,反查 raw_card 返回。
这是当前阶段的主要目标。把它做扎实是短期重点。
topic 内容检索。 Topic 配置可以 opt-in 启用检索。开启后,Storage Layer 在写消息时主存储和 SearchEngine 都写一份——payload 当作内容直接传给 SearchEngine。检索通过新接口暴露。
工程节奏上在 mq9 注册中心稳定后再做。它不是当前阶段的核心,但是 SearchEngine 长期能服务的真实场景。
未来可能的场景。 元数据检索、Connector 内容路由、Bridge 内容过滤等。SearchEngine 作为基础能力被需要的人调用。这些不是现在要做的,但抽象做对了,将来能直接复用。
几个还没想清楚的问题
诚实说,这件事还有几个问题没想透:
二进制 payload 怎么处理。 纯文本直接拆词、embedding 都没问题。二进制(图片、视频、Protobuf)当文本处理会得到无意义的索引。可能的方向是用 UTF-8 解码失败/不可打印字符比例等启发式判断是不是文本,检测到二进制就跳过索引。这个判断逻辑放在调用方做,SearchEngine 收到的就是"待索引的内容"。
长文本怎么处理。 embedding 模型有 token 长度限制(BGE-small 是 512 tokens)。超长消息要么截断要么分块。简单方案截断,复杂方案分块(一条消息对应多个向量)。默认截断,让用户配置可以开启分块。
副本怎么处理。 RobustMQ 的存储有副本,索引也有副本?简单方案是每个副本各自建索引(写入开销 N 倍)。复杂方案是 primary 建索引,副本复制索引文件。具体走哪条路根据 RobustMQ 整体的高可用模型定。
embedding 模型升级怎么处理。 模型一旦投产,所有数据的向量都是用这个模型生成的。换模型必须全量重新生成,成本很高。可能的方向是 RobustMQ 不主动升级 embedding 模型,让用户自己决定。具体机制还要想。
这些问题不影响整体方向的判断——它们都是实现细节,等真正动手做时再处理。
关于边界
最后讲清楚一件事——
检索对 RobustMQ 是个增强,不是必要能力。
RobustMQ 的核心是通信。MQTT、Kafka、AMQP、NATS、mq9 五种协议在统一存储上的能力,这是 RobustMQ 存在的理由。检索、规则引擎、Connector 这些是围绕通信的局部增强,让 RobustMQ 在某些场景下更好用。
这个边界很重要。如果 SearchEngine 试图变成通用 AI 数据平台,或者试图替代 Elasticsearch,RobustMQ 就会变成一个"什么都做但什么都不精"的东西。基础设施项目最危险的方向就是这个——分散精力扩展能力,最后核心也做不好。
守住边界意味着:
通信是核心。 MQTT、Kafka、AMQP、NATS、mq9 协议本身、协议之上的存储层、协议中立的设计——这些是必须做精的部分。性能、稳定性、协议兼容性都不能让步。
SearchEngine 是增强。 它存在的意义不是替代专业搜索引擎,是为那些"不愿部署多组件"的场景提供基础检索能力——边缘部署、中小企业、嵌入式场景、测试开发环境。这些场景里专业组件的运维成本比能力差距更重要。用户宁可接受 60 分的检索能力,也不愿为简单需求部署一个独立系统。
规则引擎、Connector 同样是增强。 让 RobustMQ 在数据集成、轻量流处理这些场景能用,但不试图打败 Flink 或 Airbyte。
这个边界长期不会变。即使 SearchEngine 演进、即使用户提出更多检索需求,能力扩展也只在四种基本能力(按 key、按 tag、全文、向量)内深化,不试图变成搜索引擎。规则引擎、Connector 也是同样的克制。
这种克制不是缺乏野心,是基础设施项目长寿的方式。Kafka 加了 Streams、Connect、Schema Registry,但 Kafka 始终是流式消息平台。PostgreSQL 加了向量、JSON、地理空间,但 PostgreSQL 始终是关系型数据库。Redis 加了模块系统、流、向量,但 Redis 始终是内存数据结构服务。这些项目都做了能力扩展,但都没有改变自己的本质定位。RobustMQ 走同样的路——核心做精,扩展做合适,不试图什么都做。
整体节奏
把上面这些综合起来,这件事的工程节奏:
短期,把 SearchEngine 做出来,目标是把 mq9 注册中心的 Agent Discover 跑通。性能、规模、稳定性的目标按 Agent Discover 的需求定。
中期,验证基础能力靠谱后,扩展到 topic 检索。可能要为 topic 检索的高负载需求做一些优化(异步队列、批量处理等)。
长期,看实际使用反馈决定演进方向。但能力范围的边界(四种基本检索)不变。
至于这件事最终能走多远——是只服务 mq9 注册中心、还是真的成为 RobustMQ 内部稳定的检索基础组件——要看实际使用反馈。当前的判断是值得做、思路成立。剩下的边做边看。
