Skip to content

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 内部稳定的检索基础组件——要看实际使用反馈。当前的判断是值得做、思路成立。剩下的边做边看。

🎉 既然都登录了 GitHub,不如顺手给我们点个 Star 吧!⭐ 你的支持是我们最大的动力 🚀