第 7 章 · 02 C++ 原生引擎与 Rust RAGFS


第 7 章 · 02 C++ 原生引擎与 Rust RAGFS

本节摘要:本章深水区。存储的「机制层」由两套原生代码承担:src/ 约 1.39 万行 C++ 是自研原生向量引擎——abi3 稳定 ABI 保证二进制兼容、SIMD 按 CPU 能力运行时分档、LevelDB 做 KV 持久化、向量与标量混合索引,可选 cuVS 上 GPU;为什么自研?嵌入式零依赖与性能。crates/ragfs 约 4.6 万行 Rust 是 RAGFS 聚合文件系统——插件化挂载(memfs/kvfs/localfs/s3fs/sqlfs/queuefs)、cache 缓存子系统(Redis/Mooncake 等可插拔 provider)、gitoxide 实现的子版本管理、ragfs-shell 命令行;ragfs-python 用 PyO3 把引擎直接嵌进 Python 进程,而 ov_cli 则是 Rust 写的 TUI 配置向导。

内容来源:原项目源码 src/(abi3_engine_backend.cpp、abi3_x86_caps.cpp、index/、store/、CMakeLists.txt)、crates/(ragfs、ragfs-python、ragfs-cache-redis、ragfs-cache-mooncake、ov_cli)

⚠️ 注意:读本章不需要会写 C++/Rust,但要看懂两个「为什么」:为什么向量引擎用 C++(算力敏感、ABI 稳定)、为什么文件系统用 Rust(并发安全、无 GC 停顿)。Python 层通过 capsule 持有 C++ 对象、通过 PyO3 类持有 Rust 对象——两座桥都不经过 HTTP,纯进程内调用。

学习目标

  1. 说清 abi3 稳定 ABI 的意义:Py_LIMITED_API 0x030A0000、capsule 命名、一个二进制跨多个 Python 小版本。
  2. 读懂引擎骨架:IndexEngine 的检索/过滤接口,PersistStore(LevelDB)与 VolatileStore 双 KV,向量+标量混合索引。
  3. 理解 SIMD 运行时检测:cpuid/xgetbv 探测 AVX2/AVX-512,CMake 分档编译,x86 caps 独立小模块。
  4. 认识 RAGFS:插件注册表、七种 FS 插件、cache 子系统与三种缓存后端 crate、git 子版本管理。
  5. 区分三个 Rust 交付物:ragfs 库、ragfs-python 绑定(进程内)、ov_cli 的 ov 二进制(TUI)。

一、为什么自研向量引擎

OpenViking 的默认向量后端叫 local——不是调用某个现成向量库,而是进程内嵌的自研引擎。src/ 目录 65 个 C++ 文件共 13,882 行,分四块:index/(向量+标量索引)、store/(KV 持久化)、common/(工具)、加两个 abi3 入口。为什么不用 FAISS/Milvus?两个理由。其一,嵌入式零依赖:OpenViking 的安装体验是 pip install openviking 一条命令,单进程内跑通——外挂向量库意味着部署拓扑、版本匹配、网络调优全部变成用户问题,与「文件系统」的产品形态背道而驰。其二,性能可控:第 4 章的目录递归检索对向量层有一套非常特殊的调用模式(search_children 按 URI 前缀圈定子项再打分、level 过滤、filter token 预算),自研引擎可以把这些模式做进索引结构,通用库只能在外围绕。

引擎入口 index_engine.h 的接口面窄而准:

class IndexEngine { public: IndexEngine(const std::string& path_or_json); int add_data(const std::vector<AddDataRequest>& data_list); int delete_data(const std::vector<DeleteDataRequest>& data_list); SearchResult search(const SearchRequest& req); std::optional<SearchResult> search_with_filter_token( const SearchRequest& req, uint64_t filter_token); int set_filter_layout(const std::vector<uint64_t>& ordered_labels); FilterResult evaluate_filter(const std::string& dsl, ...); int64_t dump(const std::string& dir); StateResult get_state(); };

注意 set_filter_layoutsearch_with_filter_token:标量过滤 DSL 可以预先求值成 filter token,检索时按 token 直取候选——租户、level、URI 前缀这些高频过滤从「逐条判断」变成「布局内查找」。detail/ 下分成 vector/(含 sparse_retrieval 稀疏召回,对应第 4 章稠密+稀疏混合检索)与 scalar/(bitmap_holder/filter/scalar_index)两支,正是「向量管像什么、标量管是什么」的 C++ 落实。

二、abi3 与 SIMD:两个工程硬功夫

abi3_engine_backend.cpp 第一行就亮牌:

#define Py_LIMITED_API 0x030A0000 #include <Python.h> ... constexpr const char* kIndexCapsuleName = "openviking.vectordb.IndexEngine"; constexpr const char* kStoreCapsuleName = "openviking.vectordb.KVStore";

Py_LIMITED_API 0x030A0000 把模块限制在 Python 3.10 的稳定 ABI 子集内:一次编译的 .pyd/.so 可以被 3.10、3.11、3.12……所有后续小版本直接加载。对需要分发二进制的开源项目,这意味着 CI 矩阵从「每 Python 版本 × 每平台」塌缩成「每平台」——发布成本的数量级差异。C++ 对象经 capsule 传给 Python(schema/engine/store/bytes_row 四种名字),重活进来先放掉 GIL:

template <typename Func> auto call_without_gil(Func&& func) -> decltype(func()) { PyThreadState* save = PyEval_SaveThread(); ...func(); PyEval_RestoreThread(save); }

向量检索是多线程重活,持着 GIL 跑等于把整个 Python 进程卡死——所有入口统一走 call_without_gil

SIMD 则是「一分编译、运行时选档」:abi3_x86_caps.cpp 是个独立小模块,用 cpuid/xgetbv 探测 CPU 的 sse3/avx/avx2/avx512f/dq/bw/vl 七档能力,把结果交给主引擎选择内核:

struct CpuFeatures { bool sse3 = false; bool avx = false; bool avx2 = false; bool avx512f = false; bool avx512dq = false; bool avx512bw = false; bool avx512vl = false; };

CMakeLists.txt 里对应着阶梯:

check_cxx_compiler_flag("-msse3" HAVE_OV_SSE3) if(HAVE_OV_SSE3) list(APPEND OV_DEFS OV_DISABLE_AVX512=1) list(APPEND OV_DEFS CROARING_COMPILER_SUPPORTS_AVX512=0) ... add_library(engine_module_x86_caps MODULE abi3_x86_caps.cpp)

基线模块守着 SSE3 兼容老机器,高端指令(AVX-512)按编译期与运行期双重条件启用——连 roaring bitmap(CROARING)的指令档都一并协调。为什么要这么麻烦?因为 abi3 模块只编译一份:不能为 AVX2 机器单独发一个 wheel,就必须把「能跑任何 CPU 的入口」与「吃满新指令的内核」拆开,运行时握手。GPU 路线同理可选:cuvs 后端(Python 侧 vectordb/index/cuvs_index.py)在配置开启时把稠密检索交给 NVIDIA cuVS,docker/cuvs-dev 与 benchmark/cuvs 为它备好环境与基准(03 节再细说)。

三、KV 持久化:LevelDB 双存储

store/ 层的抽象是 KVStore 接口 + 两个实现:PersistStore(LevelDB)与 VolatileStore(内存)。PersistStore 的接口面透着一股「认真设计过」:

class PersistStore : public KVStore { public: int exec_op(const std::vector<StorageOp>& ops) override; std::vector<std::string> get_data(const std::vector<std::string>& keys) override; int put_data(...); int delete_data(...); int clear_data() override; std::vector<std::pair<std::string, std::string>> seek_range( const std::string& start_key, const std::string& end_key) override; std::vector<std::pair<std::string, std::string>> seek_range_page( const std::string& start_key, const std::string& end_key, size_t limit, size_t max_bytes, bool start_exclusive) override; private: leveldb::DB* db_ = nullptr; };

exec_op 收批量操作序列(写放大友好),seek_range_page 带 limit+max_bytes 分页游标(第 2 章 URI 前缀遍历在 KV 层的原生形态)。选 LevelDB 而非 RocksDB 是有意的减法:单进程嵌入式场景不需要 SST 多层与后台 compaction 的全部复杂度,依赖越少越符合「零依赖」目标。VolatileStore 支撑临时/易失集合。dump(dir) 一条命令把引擎状态导出目录——ovpack 快照(03 节)的底层正是它。

exec_op 的入参 StorageOp 是个操作序列而非单条写:上层一次入库动辄上千条向量记录,批量提交既省写放大也省锁竞争;seek_range_pagemax_bytes 参数比 limit 更贴心——按字节预算分页,调用方不会因为某行超大而爆内存。这两个签名放在一起看,能读出一种「为上层调用模式定制接口」的自觉:第 4 章 search_children 的前缀圈定、ovpack 的全量导出,在 KV 层都有原生的对应物,不需要 Python 侧做 O(n) 的过滤或分页模拟。

四、RAGFS:Rust 聚合文件系统

上一节说过 VikingFS 封装 AGFS——AGFS 的现代实现就是 crates/ragfs:78 个 Rust 源文件、45,720 行。lib.rs 的自我定位:

//! RAGFS provides a unified filesystem abstraction that allows multiple //! filesystem implementations (plugins) to be mounted at different paths. //! It is consumed in-process through the Rust binding (`ragfs-python`).

core/ 定义 FileSystem/ServicePlugin trait 与 PluginRegistry,plugins/ 下七种实现各占一席:memfs(内存,测试与易失数据)、kvfs(KV 映射成文件)、localfs(本地磁盘)、s3fs(对象存储)、sqlfs(关系库)、queuefs(队列即目录,上一节 /queue 挂载点的本体)、serverinfofs(运行时信息)。挂载模型就是 Unix 的 mount 语义:不同后端挂到不同路径,上层看到一棵统一的树——「聚合文件系统」由此得名。cache/ 子系统带 feature 开关,envelope/policy/provider/wrapper/metrics 一套接口之外,缓存 provider 拆成独立 crate:ragfs-cache-redis、ragfs-cache-mooncake(火山引擎 Mooncake)、ragfs-cache-yuanrong——按需编译,不拖累核心。git/ 子系统是子版本管理:基于 gitoxide(gix-* 系列 crate)实现内容寻址对象存储、ref 存储、commit/checkout/历史枚举,配合 .ovgitignore(OVGITIGNORE_PATH,规则与 gitignore 同构)——「给上下文树做 git」的底层在这里,而不是 shell 出去调 git 命令。另有 crypto/(与 openviking/crypto 对应的原生加密)、lock/(上一节 pathlock 的真身)、multibackend/shape/,以及 ragfs-shell 二进制([[bin]] name = "ragfs-shell")提供命令行直查。

五、PyO3 桥与 ov_cli:两个 Rust 交付物

ragfs-python 把上述引擎嵌进 Python:

//! Provides `RAGFSBindingClient`, a PyO3 native class that is //! API-compatible with the existing Go-based `AGFSBindingClient`. //! This embeds the ragfs filesystem engine directly in the Python //! process (no HTTP server needed).

关键句是「no HTTP server needed」:历史上 AGFS 曾是独立 Go 服务(Python 经 HTTP 访问),ragfs-python 用 PyO3 把引擎直接嵌进进程——单机部署少一个组件、少一跳网络;需要服务化时同款引擎又能以 ragfs-shell/HTTP 形态独立部署。绑定层细节考究:初始化时缓存 Python 的 LockAcquisitionError 类(static LOCK_ACQUISITION_ERROR_TYPE: OnceLock<Py<PyType>>),让 Rust 抛的锁错误与 Python 异常同型;tracing 日志可热切换输出文件并与 Python 侧共享。第三个交付物 crates/ov_cli 是纯 Rust 的 ov 命令(npm 上叫 @openviking/cli,cargo install --path crates/ov_cli 亦可装),README 第一句就是「Use it to configure an OpenViking endpoint, import resources, browse viking:// paths...」——TUI 交互式配置向导(ov config)、资源导入、路径浏览一站办齐。Python 的 openviking_cli 与 Rust 的 ov_cli 是双前台,服务端核心仍在 Python+C+++Rust 三明治里。

三明治各层的行数与职责合个总账:Python openviking/ 约 17.2 万行管语义与编排,C++ src/ 约 1.39 万行管向量算力,Rust crates/ 约 9.9 万行管文件系统与 CLI——每层都用了最顺手的那把刀:语义迭代快用 Python,数值与 ABI 用 C++,并发与系统编程用 Rust。这个分层不是炫技,而是把第 1 章的「文件系统范式」翻译成系统能力时的自然分工。

💡 漫游要点:这一层回答「文件系统范式的性能账怎么付」。C++ 引擎三件宝:abi3 稳定 ABI(一份二进制吃遍 Python 版本)、运行时 SIMD 分档(x86 caps 握手,基线兼容+高端提速)、LevelDB 嵌入式持久化(分页 seek 即 URI 前缀遍历);filter token 把高频标量过滤预编译进布局。RAGFS 四件套:插件化挂载(七种 FS)、可插拔缓存(Redis/Mooncake 独立 crate)、gitoxide 子版本、PyO3 进程内嵌(兼容旧 Go 客户端 API 而 eliminated HTTP)。共同的哲学:Python 写语义,原生层扛机制,桥上零 HTTP

本节要点回顾

  • 自研两理由:嵌入式零依赖(pip install 即用,无外挂拓扑)与调用模式可控(search_children 前缀圈定、level 过滤、filter token 均为内建能力)。
  • src/ 13,882 行四块:index(vector+scalar 混合、sparse_retrieval 稀疏召回)、store(PersistStore=LevelDB/VolatileStore=内存)、common、两个 abi3 入口;IndexEngine 接口:add/delete/search/search_with_filter_token/set_filter_layout/evaluate_filter(dsl)/dump。
  • abi3:Py_LIMITED_API 0x030A0000 稳定 ABI,capsule 四名(IndexEngine/KVStore/Schema/BytesRow),call_without_gil 全覆盖;SIMD 由 abi3_x86_caps 用 cpuid/xgetbv 探测七档,CMake check_cxx_compiler_flag 分档、OV_DISABLE_AVX512 与 CROARING 协调,基线 SSE3;cuVS 为可选 GPU 后端。
  • KV:exec_op 批量操作、seek_range_page 分页(limit+max_bytes+start_exclusive),LevelDB 做减法;dump 支撑 ovpack。
  • RAGFS 45,720 行:PluginRegistry+七插件(memfs/kvfs/localfs/s3fs/sqlfs/queuefs/serverinfofs),cache feature+三 provider crate(redis/mooncake/yuanrong),git 子系统=gitoxide 对象/ref/commit/checkout+.ovgitignore,ragfs-shell 独立二进制。
  • ragfs-python:PyO3 进程内嵌、API 兼容旧 Go AGFSBindingClient、锁异常跨语言同型;ov_cli=Rust ov 命令(npm @openviking/cli),TUI 向导 ov config。

下一节:03 四种向量后端与 ovpack 快照——回到 Python:vectordb_adapters 的 4+1 种后端如何一行配置切换,ovpack 如何把整个上下文数据库打包成单文件,并与 Hermes、semantica 的后端抽象做三方对照。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U