位置:首页 > 技术资讯 > Milvus 3.0原生TEXT类型与LOB高效管理原始文本指南

Milvus 3.0原生TEXT类型与LOB高效管理原始文本指南

时间:2026-08-21  |  作者:多维游侠  |  阅读:0

Milvus 3.0借助原生TEXT与LOB存储,一举解决向量数据库+外部存储解耦架构的痛点,实现原始文本与索引的协同高效管理。
核心要点如下:
1. 早期解耦架构存在的问题:数据不一致、链路过长。
2. Milvus 3.0原生TEXT与LOB的整合存储方案。
3. TEXT类型支持的混合检索和原子操作优势。

Milvus 3.0原生TEXT类型与LOB高效管理原始文本指南_wishdown.com
在AI检索系统里,Embedding能帮我们快速找到语义相近的内容。不过,从TEXT_MATCH、BM25等词法检索,到reranking、答案生成、高亮和审计,系统还是得保存并访问原始文本。

正因如此,怎样让原始文本能和索引一起被存储、检索和管理,成了现代数据库建设的关键命题。早期,行业常用向量数据库 + 外部存储的解耦架构来解决这个问题:向量数据库只存放Embedding和简单的Metadata,而长文档正文、代码段或日志则存放在S3、MongoDB或Elasticsearch中。在这种架构下,一旦要更新或删除文档,就得同时操作外部存储与向量数据库。然而,在分布式网络中,这种方式很容易引发数据不一致,导致向量检索命中但取不到正文(或者取错版本)。与此同时,混合检索还进一步放大了这个问题:要是原始文本不在向量数据库内,BM25倒排索引的构建与全文本过滤就得依赖外部组件,链路冗长且难以保证原子性。既然如此,为何不直接把正文放进数据库呢?原因是,如果只是简单地把巨量原始文本当作普通字符串列塞进向量数据库,却仍按普通内联字符串处理,大Payload就会进入内存、flush、compaction和I/O的各个环节:百KB到数MB的长文本会迅速挤爆内存缓冲区,还会在数据合并重构(Compaction)时引发严重的磁盘与网络写放大。Milvus 3.0引入DataType.TEXT和LOB存储路径,就是为了解决这个问题:让长文本像稠密向量、稀疏向量和标量字段一样,成为Milvus中的一等公民,并纳入对象存储的完整生命周期管理。具体来说:
  • TEXT可用于存储文档正文、RAG chunk、日志、源代码、对话等较长的文本内容。
  • 同一个TEXT字段能与analyzer、text_match、BM25、稠密向量以及混合搜索配合使用。
  • 较短的文本仍直接存放在Segment中;较大的文本则转为LOB文件,并在Segment中保存引用。
  • Compaction时,若现有LOB文件仍值得保留,可直接复用,不必重新写入全部正文。
  • DataCoord中的LOB GC会根据Manifest的引用关系和安全时间窗口,清理已失去引用的孤儿文件。

01

为什么不能把TEXT当成一个更大的VARCHAR

先区分两个容易混淆的概念。对于标签、状态、名称、类别、ID等短元数据,VARCHAR仍是更合适的选择。TEXT则适合面向更长的内容,比如:RAG chunk文本;文档正文;源代码;日志;客服或支持对话;多语言文本;需要参与全文检索或混合检索的字段。例如,我们可这样定义Schema:
schema.add_field(    field_name="content",    datatype=DataType.TEXT,    nullable=True,    enable_analyzer=True,    enable_match=True,    analyzer_params={"tokenizer": "standard"},)
这里的content不仅能正常写入和返回,还能参与analyzer、text_match、BM25等文本检索能力。这意味着从数据模型来看,正文终于能和向量、标量字段放在同一个Collection里。但这马上带来了下一个问题:为何Milvus还需要为TEXT单独设计LOB,而不是继续像普通Segment列一样保存它呢?原因主要有四个:
  1. Growing Segment的内存压力会迅速放大。Milvus要让刚写入的数据立即可查,QueryNode中的Growing Segment就必须能访问最近写入的数据。对于普通标量字段,这部分数据通常不大。但如果一行数据携带几百KB甚至数MB的正文,情况就大不一样了。此时决定内存占用的可能不再是向量,而是文本Payload本身。
  2. 写入链路可能重复保存同一份大文本。传统写入链路可简化为:
    WAL -> StreamingNode write buffer -> object storage
    与此同时,为了查询Growing Data,QueryNode本身也需要访问刚写入的数据。于是,对于大文本来说,一个明显的问题出现了:如果StreamingNode为了等待flush再保留一份完整Payload,而查询侧已持有一份,就相当于同一份正文在内存中被重复保存。
  3. Compaction带来的写放大。Compaction的目的通常是整理Segment、处理删除记录并重组数据。但如果长文本和其他字段始终捆绑在同一个Segment文件中,哪怕真正发生变化的只有少数几行,Compaction仍可能不得不重新搬运大量完全没有变化的正文。例如,一个Segment中有几GB的原始文本,而这次Compaction实际只删除了其中很少一部分记录。理想状态下,我们只需更新这些记录的引用关系。如果正文完全内联,则可能不得不把几GB数据重新写一遍。
  4. 与文本无关的查询也承担了额外I/O。并非每一次向量搜索都需要返回全文。很多查询只需要向量、主键、标量过滤字段,或者少量指定的输出字段。如果大文本始终内联在Segment列文件中,这些文件会因正文而变得更大,即便一次查询根本不会访问这些内容。
基于以上背景,如何让大文本进入Milvus,又尽量不干扰原有的向量和标量数据路径,就是我们引入LOB存储的原因。

02

混合存储:短文本内联,大文本走LOB

为了在“读取性能”与“存储开销”之间取得最佳平衡,Milvus 3.0并未把所有TEXT都无条件拆成独立文件,而是在存储底层通过混合存储布局引入了动态的分层处理机制。系统会根据文本Payload体积自动切换保存策略:如果一段文本本身很短,把它放在Segment里直接读取通常更简单;只有当Payload足够大时,才把它从主数据路径中拆出去。(当前默认阈值为64KiB,具体参数可配置)
small TEXT value -> inline byteslarge TEXT value -> LOB file + reference
也就是说,TEXT是应用层的数据类型,而inline和LOB是底层根据Payload大小采用的两种存储方式。从概念上可理解成:
segment manifest  normal column groups:    pk, vector, scalar fields  TEXT reference column:    row 1 -> inline bytes    row 2 -> LOB ref(file_id, row_offset)    row 3 -> LOB ref(file_id, row_offset)partition-level lobs/  {field_id}/_data/{file_id}.vx
如此一来,小文本继续保持Inline状态,直接保存在Segment中,常规读取仍然轻量;大文本则移到独立的LOB文件中,Segment只保留对应引用。内存中的Growing Segment和写缓冲区不必再常驻巨量Payload,显著降低了内存压力。同时也为后续的LOB文件复用和生命周期管理奠定了基础。

03

Compaction:能复用正文,就不要重新写一遍

数据落盘以后,Segment仍会发生删除和Compaction。如果每次Compaction又把所有LOB重写一遍,会造成较大的I/O放大。

假设一个 Segment 中保存了大量文档。现在其中 5% 的记录被删除,需要进行 Compaction。

对于主键、标量和向量数据,生成一个新的 Segment 很正常。但对于几百 MB 甚至几 GB 的正文来说,如果剩下 95% 的内容实际上完全没有变化,再复制一次就没有太大意义。

LOB 将文本 Payload 从普通 Segment 文件中拆出去之后,Milvus 就可以实现让新的 Segment 可以被重新组织引用,而原来的 LOB 文件继续使用。

当然,并不是所有情况下都值得复用。如果一个 LOB 文件中绝大多数记录已经被删除,继续保留整个文件反而会浪费空间。

因此, Milvus 引入了一个重要指标:hole ratio

hole_ratio = unused_lob_rows_or_bytes / total_lob_rows_or_bytes
它描述的是一个 LOB 文件中已经不再使用的数据比例。根据实际情况,Compaction 可以采取不同策略:
  • REUSE_ALL:如果 hole ratio 较低,继续使用已有 LOB 文件,只复制或更新引用。
  • REWRITE_ALL:如果 hole ratio 较高,只把仍然有效的文本重新写入新的 LOB 文件。
  • SKIP:对于仅处理删除数据的 L0 compaction,不需要移动 LOB 文件。
这样就避免了一个典型的写放大场景:明明只删除了少数几行,却不得不重新写入数 MB,甚至数 GB 的文档内容。

04

Read Path:对查询层保持透明

把 TEXT 分成 inline 和 LOB 两种布局,会让存储层复杂一些。但这种复杂度不应该暴露给应用。在Milvus3.0中,查询执行层仍然是按照行读取字段。至于这一行文本究竟直接存在 Segment 中,还是要从 LOB 中获取,由底层存储层处理:
if reference is inline:    return inline byteselse:    decode file_id + row_offset    read from LOB Vortex file
当一次查询需要读取多条 LOB 数据时,Milvus 还可以先按照 file_id 对引用进行分组。这样,同一个 LOB 文件对应的请求可以复用 Reader,再根据不同的 row_offset 找到对应文本,而不是每读取一行都重新打开文件。从用户和应用程序的角度看,TEXT 依然可以像普通字段一样出现在搜索结果中,不需要修改原有业务逻辑。

05

GC:文件要能复用,也要能够安全删除

一旦 LOB 文件可以脱离单个 Segment 独立存在,新的问题也随之出现:什么时候可以确定一个 LOB 文件已经没有任何人需要,可以安全删除?这里不能简单地看“最近有没有 Segment 使用它”。这与分布式系统中的提交顺序有关。LOB 文件会先写入对象存储,随后对应的 Segment Manifest 才会提交并变得可见。因此,如果一个节点恰好在 LOB 文件写入之后、Manifest 提交之前崩溃,对象存储中就可能残留没有任何引用的孤儿文件。Milvus 在 DataCoord 中提供了专门的 LOB GC 来处理这类情况:
  1. 扫描仍然有效的 Segment Manifest 和引用 Binlog。
  2. 构建当前可达的 LOB file_id 集合。
  3. 枚举对象存储中的 LOB 文件。
  4. 删除已经没有引用、并且早于安全时间窗口的文件。
安全时间窗口的作用,是避免误删仍处于 flush 或 compaction 过程中的文件——这些文件的数据可能已经写入,但元数据尚未来得及提交。这样,即使发生节点崩溃、重试或部分写入,LOB 文件的生命周期仍然可以被安全管理。

06

对用户来说,TEXT 仍然是普通字段

前面讨论了很多底层机制,但这些机制有一个共同目标:不要把存储复杂度传递给使用 Milvus 的应用。应用不需要判断某条 TEXT 是 inline 还是 LOB,也不需要自己创建、维护或者删除 LOB 文件。正常定义字段即可。存储并返回 TEXT
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)schema.add_field(field_name="content", datatype=DataType.TEXT, nullable=True)schema.add_field(field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=768)
client.insert(    collection_name="docs",    data=[        {            "id": 1,            "content": "A long document body ...",            "embedding": embedding,        }    ],)
搜索时可以像其他字段一样返回正文:
client.search(    collection_name="docs",    data=[query_embedding],    anns_field="embedding",    limit=10,    output_fields=["content"],)
使用 text_match如果字段启用了对应的 analyzer 和 match 能力,还可以直接进行文本匹配:
client.query(    collection_name="docs",    filter='text_match(content, "vector database")',    output_fields=["id", "content"],)
使用 BM25 全文搜索同一个 TEXT 字段也可以作为 BM25 Function 的输入:
schema.add_field(field_name="content_sparse", datatype=DataType.SPARSE_FLOAT_VECTOR)schema.add_function(Function(    name="content_bm25",    function_type=FunctionType.BM25,    input_field_names=["content"],    output_field_names=["content_sparse"],))
index_params.add_index(    field_name="content_sparse",    index_type="SPARSE_INVERTED_INDEX",    metric_type="BM25",)
client.search(    collection_name="docs",    data=["vector database full text search"],    anns_field="content_sparse",    search_params={"metric_type": "BM25"},    limit=10,    output_fields=["content"],)
总而言之,TEXT 是用户的数据类型,LOB 是 Milvus 的存储策略。用户管理的是文本字段,而不是 LOB 文件。

07

从内联字符串到 TEXT + LOB,到底改变了什么?

回到文章开头的问题。Milvus 过去并非完全不能保存字符串。只不过,我们还需要考虑,当字符串变成长文档之后,如何避免它一路放大 Growing Data、写缓冲区、Compaction 和对象存储的成本。TEXT + LOB 的设计,本质上是把大文本 Payload从 Segment 主数据路径中拆出来,同时又不改变上层的数据模型。Milvus 3.0原生TEXT类型与LOB高效管理原始文本指南_wishdown.com对很多需要混合检索的系统来说,它意味着应用不再需要为了不同的检索方式,把同一份文本拆到多套存储和索引系统中分别维护。这样一来,向量、文本和 Metadata 可以围绕同一条数据完成写入、更新、查询和删除,整个检索链路也会更简单。

作者介绍

Milvus 3.0原生TEXT类型与LOB高效管理原始文本指南_wishdown.com

张露

Staff Software Engineer at Zilliz

阅读推荐官宣开源|Milvus 3.0 正式发布Milvus 3.0 开源解读之backfill|亿级 AI 数据,如何做高效特征回填Milvus 3.0 开源解读|从Kafka、Pulsar到Woodpecker,数据库如何低延迟、低成本的写入Milvus 3.0 开源解读之Manifest|AI 数据管理,应该彻底放弃文件中心架构Milvus 3.0 开源解读之,数据库原生聚合排序如何取代应用侧Pandas胶水代码Milvus 3.0开源解读之Regex |从=~到NGRAM,如何选择最优性价比的正则过滤Milvus 3.0 开源解读之Snapshot|无需复制embedding数据的Milvus Collection视图Milvus 3.0 开源解读|自定义词典如何优化BM25 与Text Match 的专业词理解能力
Milvus 3.0原生TEXT类型与LOB高效管理原始文本指南_wishdown.comMilvus 3.0原生TEXT类型与LOB高效管理原始文本指南_wishdown.com

登录查看剩余 70% 内容

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多