跳到主要内容
AI News HubLIVE
站内改写3 分钟阅读

SmithDB 全文搜索:为对象存储设计倒排索引

文章摘要

SmithDB 在对象存储上为大型、深层嵌套的 JSON 文档(代理追踪数据)提供全文搜索和 JSON 过滤,中位(P50)延迟为 400 毫秒。本文介绍了其倒排索引的设计、面临的独特挑战(数据特征和对象存储约束),以及从第一性原理出发的演进过程。

SmithDB 全文搜索:为对象存储设计倒排索引
报告错误

纠错通道尚未开通,可先复制下方文章信息留存。

查看更正说明
直接读正文

SmithDB 是 LangChain 旗下用于存储和查询代理追踪数据的数据库系统。它需要在对象存储上对大型、深层嵌套的 JSON 文档执行全文搜索和 JSON 过滤,同时保持中位(P50)延迟在 400 毫秒。这与传统的日志搜索引擎截然不同,因为代理追踪数据中的单个文档可能非常庞大,且内容字段(inputs 和 outputs)通常占据绝大部分字节,大小可达 1MB 以上,甚至数百 MB 未压缩。

全文搜索本身并非新问题,Lucene 已有二十年历史,Tantivy 和 Quickwit 也已将搜索和索引推向对象存储。然而,SmithDB 团队决定从第一性原理出发重新设计,因为索引代理追踪数据面临两个独特挑战。

第一个挑战是代理追踪数据的特性。每个 LangSmith 事件中,inputs 和 outputs 字段占据了绝大多数字节,通常超过 1MB,有些甚至达到数百 MB 未压缩。这些内容列比身份、时间戳和其他元数据列大几个数量级。此外,随着 LLM 上下文窗口增大和代理运行时间变长,这些负载还在持续增长。这颠覆了传统搜索索引的经济学:通常日志引擎索引数十亿个小文档,索引大小约为源数据的 1.25 倍;但 SmithDB 索引数十亿个巨大文档,源数据与索引的比例接近 1:1.9。这意味着没有索引的内容过滤会灾难性地慢,词频遵循齐夫分布(少数词出现在几乎所有文档中,而长尾词只出现一两次),并且查询需要支持多种模式(按路径、按值、按自由文本)。

第二个挑战是对象存储。SmithDB 将所有持久数据保存在对象存储中,使计算相对无状态,并通过添加节点来扩展,而无需管理本地磁盘。查询成本大致与(向对象存储发出的请求数)×(每个请求读取的字节数)成正比。每个对象存储请求的延迟在几十到几百毫秒之间,且每个请求的吞吐量有限,因此提前获取大的倒排列表或位置列表可能会主导查询时间。SmithDB 倒排索引的每个方面,从存储布局到查询执行,都围绕这些约束设计。

SmithDB 的查询接口可归结为三种谓词家族:json_key(路径存在)、json_key_search(键值搜索)和 search(全文搜索)。json_key 检查文档是否包含某个键路径,支持对键路径本身使用 LIKE 模式,如 json_key(inputs, "author.%")。json_key_search 检查某个键的值是否匹配给定值,可以是单个标记或多标记短语,短语变体要求相邻匹配。search 则在文本列或所有 JSON 值中搜索查询词,无论路径如何。

倒排索引是支撑所有搜索库的数据结构,其核心概念包括:术语(索引单元,如 JSON 路径、键值或文本标记)、倒排列表(包含该术语的文档 ID 的有序集合)和位置(术语在文档中出现的位置,用于短语搜索)。例如,对于五个文档,索引为每个术语维护一个条目,指向包含它的文档。查询时,如 search("deep agents"),系统会求 deep 和 agents 的倒排列表的交集,得到同时包含两者的文档 ID,而无需扫描负载。位置信息使得短语搜索成为可能,例如 search("langsmith engine") 只匹配 langsmith 和 engine 相邻出现的文档。

SmithDB 团队没有直接采用 Tantivy,尽管它深受 Tantivy 启发。原因有三:Tantivy 围绕 mmap 构建,假设本地磁盘和微秒级随机 I/O,而 SmithDB 在对象存储上,每次往返约 100 毫秒,布局和合并决定查询延迟;Tantivy 是独立搜索栈,而 SmithDB 希望搜索通过 Apache DataFusion 和 Vortex 的同一扫描管道下推;Tantivy 的文档 ID 是段内局部且按插入顺序分配,合并时会重新编号,而 SmithDB 需要索引直接指向 Vortex 数据文件中的行位置,避免翻译表。

Vortex 是 SmithDB 使用的可扩展列式文件格式,支持可插拔编码和自定义文件布局。每次读取使用统计信息修剪整个行组,将幸存行过滤为掩码,并仅投影需要的列。Vortex 中 I/O 的单位是段(连续物理字节范围),其 I/O 调度器会将相邻段读取合并为一个请求,合并 1MB 间隙内的读取,最大窗口 16MB,从而将顺序访问模式映射到极少的对象存储 GET 请求。

最初的倒排索引实现几乎是教科书式的直译。使用两个列(term_key 用于路径,term_value 用于标记)服务所有三种查询形状,倒排列表存储为 List 单元,位置存储为 List>。依赖 Vortex 默认值:术语列使用 FSST 编码,倒排列表和位置使用位打包编码,并采用分区存储布局以便查询时修剪。位置(短语搜索所需)比其他列大一个数量级,因此索引单独存放在与核心运行数据不同的文件中,以解耦索引构建和合并与核心写入路径。

然而,在规模扩大后出现了三个问题。首先,无法按术语控制编码:Vortex 为整个列选择编码,而不是按术语选择,因此一个常见标记(如 agent、langchain)会迫使整个块中所有术语使用更大的位宽,导致位打包效果差。其次,固定大小的行组对术语倾斜不敏感:按固定术语数批处理行组,导致一个高频术语可能使一个行组压缩后超过 100MB,而另一个只有几 MB,查询时变成一次超大的对象存储 GET,合并时则导致超大的内存解码。第三,合并需要重塑位置:合并两个段时,需要解码完整的 positions List>,将内部列表重新排列到新文档顺序,并重新计算每个外部偏移量,CPU 时间和分配在压缩时激增。由于位置占索引字节的 70% 以上,这是压缩的主要成本。

第二版布局通过将组织单位从“每行组 N 个术语”改为“按字节预算的行组”,并自主控制每列的字节布局而非直接依赖 Vortex 默认值,解决了上述三个问题。具体细节在博客的第二部分继续展开。

展开要点与分析

文章情报

工程师进阶

要点

  • SmithDB 为代理追踪数据设计倒排索引,支持路径存在、键值搜索和全文搜索三种查询模式。
  • 代理追踪数据具有大负载(输入/输出常见 1MB+)和齐夫分布的词频,导致索引膨胀和长尾查询问题。
  • 对象存储的高延迟(约 100ms 每次请求)和按请求计费要求索引布局最小化 IO 请求。
  • 初始方案直接移植教科书式倒排索引,但在编码控制、行组大小和合并成本上遇到瓶颈。
  • 第二版布局改为按字节预算划分行组,并自主控制每列的字节布局,以解决上述问题。

要点与分析由自动化流程生成,可能有误,请结合原始来源核实。