news 2026/10/21 8:02:29

Cactus Index FFI 详解:基于内存映射的端侧向量数据库与 RAG 检索 API

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cactus Index FFI 详解:基于内存映射的端侧向量数据库与 RAG 检索 API
  • 推理引擎
  • 模型推理服务
  • 算子库
  • 模型量化
  • 边缘计算

【免费下载链接】cactus

Quantization, kernels, runtime and inference engine for mobiles, wearables, smart home and robots.

项目地址:https://gitcode.com/gh_mirrors/cactus12/cactus
点击查看免费下载

Cactus Index 是 Cactus 项目中面向移动端、可穿戴设备、智能家居与机器人场景设计的端侧向量数据库,通过一套干净的 C FFI(Foreign Function Interface)暴露给上层应用。本文基于 docs/cactus_index.md 展开,完整讲解索引的存储布局、七个核心 C API 的签名与约束、查询选项 JSON 格式、RAG 检索与 Embedding 模型迁移等实战用法,并结合 cactus-engine 的源码实现与测试用例,深入剖析内存映射、FP16 存储、余弦相似度与软删除/压缩回收等底层原理。读完本文,你将能够直接在 C/C++ 项目中集成端侧向量检索,或将同样的调用模式迁移到 Kotlin、Swift、Dart、Rust 等语言绑定。

一、Cactus Index 是什么

Cactus Index 是 Cactus 推理引擎(cactus-engine 尚需确认,实际位于 cactus-engine)内置的轻量级向量数据库模块。它的设计目标非常明确:在设备本地存储 Embedding 向量与文档正文,支持余弦相似度检索,为 RAG(检索增强生成)提供检索环节,且不依赖任何外部数据库服务。

与云端向量库不同,Cactus Index 从设计之初就面向资源受限设备:

  • 内存映射文件存储:索引与数据均以文件形式组织,通过mmap映射到进程地址空间,避免频繁的磁盘 I/O;
  • FP16 半精度存储:Embedding 以 FP16 存储,节省一半显存/内存占用;
  • 自动归一化:所有写入的 Embedding 自动归一化为单位长度,使内积等价于余弦相似度;
  • C FFI 接口:函数签名全部以 C 语言暴露,天然可被 Python、Kotlin/Swift、Dart、Rust、React Native 等语言通过 FFI 调用。

1.1 文件布局

索引初始化后,会在指定的目录中生成两个文件(定义于 cactus-engine/src/index.cpp 的Index::Index构造函数):

文件内容编码
index.binEmbedding 向量(FP16)+ 每条文档指向data.bin的偏移量二进制
data.bin文档正文与元数据UTF-8

两个文件必须同时存在或同时不存在。索引打开时会校验文件头中的魔数与版本号:MAGIC = 0x43414354、VERSION = 1(定义于 cactus-engine/src/engine.h)。若打开已存在索引时传入的embedding_dim与文件头中记录的维度不一致,会直接报错Embedding dimension mismatch。

1.2 数据结构的二进制形态

从源码 cactus-engine/src/engine.h 可以看到索引内部的数据组织:

  • IndexHeader:魔数、版本、Embedding 维度、文档数量,位于index.bin头部;
  • IndexEntry:doc_id(int32)、data_offset(uint64,指向data.bin的偏移)、flags(bit 0 为删除墓碑标记),其后紧跟该文档的 FP16 Embedding;
  • DataHeader:魔数与版本,位于data.bin头部;
  • DataEntry:content_len(uint16)与metadata_len(uint16),其后依次是正文与元数据。

正因为content_len/metadata_len是 uint16,单条文档正文与元数据上限均为 65535 字节,这是 FFI 层约束的根源。

二、核心类型与错误处理

2.1 句柄类型cactus_index_t

typedef void* cactus_index_t;

这是一个不透明指针类型(定义于 cactus-engine/cactus_engine.h)。在 FFI 实现层,它实际指向一个CactusIndexHandle结构体,内部持有std::unique_ptr<cactus::engine::index::Index>(见 cactus-engine/src/index_ffi.cpp)。应用层只需要将其当作不透明句柄传递,不要尝试解引用。

2.2 错误信息获取cactus_get_last_error

几乎所有返回-1的函数都会把错误原因写入内部缓冲,应用可以通过cactus_get_last_error()读取最近一次错误的字符串描述(声明于 cactus-engine/cactus_engine.h)。例如:

fprintf(stderr, "Failed to add documents: %s\n", cactus_get_last_error());

该 API 是排查参数错误(维度不匹配、缓冲区过小、ID 重复)的第一手工具,本文后续示例均沿用此模式。

三、核心函数全解析

3.1cactus_index_init:初始化或打开索引

cactus_index_t cactus_index_init( const char* index_dir, size_t embedding_dim );

参数:

参数说明
index_dir索引文件存放目录(不可为 NULL 或空字符串)
embedding_dimEmbedding 维度;已有索引必须与该维度一致

返回:成功返回索引句柄,失败返回NULL。

约束(对应 cactus-engine/src/index_ffi.cpp 的实现):index_dir为 NULL 或空、embedding_dim为 0 时均返回 NULL 并记录错误;目录不存在时,实现会用open(..., O_RDWR | O_CREAT, 0644)自动创建两个文件并写入文件头(首次调用相当于"创建"),因此目录路径本身需要预先存在或可由系统创建。

示例:

cactus_index_t index = cactus_index_init("./my_index", 768); if (!index) { fprintf(stderr, "Failed to initialize index\n"); return -1; }

3.2cactus_index_add:批量写入文档

int cactus_index_add( cactus_index_t index, const int* ids, const char** documents, const char** metadatas, const float** embeddings, size_t count, size_t embedding_dim );

参数:

参数必填说明
index是索引句柄
ids是文档 ID 数组,整数且必须唯一
documents是文档正文数组(UTF-8),数组本身不可为 NULL,但单个documents[i]可为 NULL(按空串存储)
metadatas否元数据数组(UTF-8),可为 NULL,单个metadatas[i]也可为 NULL
embeddings是指向各 Embedding 向量的指针数组,每个embeddings[i]都不可为 NULL
count是本次写入的文档数,必须 > 0
embedding_dim是Embedding 维度,必须 > 0 且与索引维度一致

约束汇总(对应 cactus-engine/src/index_ffi.cpp 与 cactus-engine/src/index.cpp 的validate_documents):

  • 文档 ID 必须唯一,且不能与索引中已存在的 ID 重复(否则报Document ID already exists);
  • 单条正文与元数据均不能超过 65535 字节;
  • Embedding 维度必须等于索引维度;
  • 批量内不允许出现重复 ID(报Duplicate document ID in input)。

返回:0成功,-1失败。

示例(带元数据):

int ids[] = {1, 2}; const char* docs[] = {"AI is transforming technology", "Machine learning enables predictions"}; const char* metas[] = {"{\"source\":\"wiki\"}", "{\"source\":\"blog\"}"}; float emb1[768] = {0.1, 0.2, 0.3, /* ... */}; float emb2[768] = {0.4, 0.5, 0.6, /* ... */}; const float* embeddings[] = {emb1, emb2}; int result = cactus_index_add(index, ids, docs, metas, embeddings, 2, 768); if (result != 0) { fprintf(stderr, "Failed to add documents: %s\n", cactus_get_last_error()); }

示例(无元数据,metadatas传 NULL):

int ids[] = {1, 2}; const char* docs[] = {"Document one", "Document two"}; float emb1[768] = {0.1, 0.2, 0.3, /* ... */}; float emb2[768] = {0.4, 0.5, 0.6, /* ... */}; const float* embeddings[] = {emb1, emb2}; // Pass NULL for metadatas int result = cactus_index_add(index, ids, docs, NULL, embeddings, 2, 768);

3.3cactus_index_delete:软删除

int cactus_index_delete( cactus_index_t index, const int* ids, size_t ids_count );

参数:

参数说明
index索引句柄
ids待删除文档 ID 数组
ids_countID 数量,必须 > 0

行为:这是软删除。实现层只把对应IndexEntry.flags的 bit 0 置 1(墓碑标记),并从内存中的doc_id_map_移除映射(见 cactus-engine/src/index.cpp),磁盘空间并不会立即释放,需要后续调用cactus_index_compact才能真正回收。

约束:每个 ID 必须存在于索引中;同一批次内 ID 不能重复。删除不存在的 ID 会返回-1并记录Document ID not found。

返回:0成功,-1失败。

示例:

int ids[] = {1, 2, 3}; int result = cactus_index_delete(index, ids, 3); if (result != 0) { fprintf(stderr, "Failed to delete documents: %s\n", cactus_get_last_error()); }

3.4cactus_index_get:按 ID 读取文档

int cactus_index_get( cactus_index_t index, const int* ids, size_t ids_count, char** document_buffers, size_t* document_buffer_sizes, char** metadata_buffers, size_t* metadata_buffer_sizes, float** embedding_buffers, size_t* embedding_buffer_sizes );

这是参数最复杂的函数,核心设计是**"需要哪些字段就传哪些缓冲区,其余传 NULL"**。

参数:

参数必填说明
index,ids,ids_count是索引句柄、ID 数组、ID 数量
document_buffers否预分配的正文缓冲区数组;传 NULL 表示不取正文
document_buffer_sizes条件与document_buffers成对出现,输入为各缓冲区容量(字节),输出为实际写入字节数(含\0终止符)
metadata_buffers否预分配的元数据缓冲区数组;传 NULL 表示不取元数据
metadata_buffer_sizes条件同上,输入为容量、输出为实际大小(含\0)
embedding_buffers否预分配的 Embedding 缓冲区数组;传 NULL 表示不取向量
embedding_buffer_sizes条件注意单位是 float 个数而不是字节;输入为容量、输出为实际写入的 float 数

返回值语义:0成功;-1失败——其中最重要的失败场景是缓冲区过小(此时不拷贝任何数据)。实现会先对全部条目做容量预检,任一不足即整体返回-1(见 cactus-engine/src/index_ffi.cpp)。

示例:读取全部字段

int ids[] = {1, 2}; char* docs[2]; char* metas[2]; float* embs[2]; // Allocate buffers for (int i = 0; i < 2; i++) { docs[i] = malloc(65536); metas[i] = malloc(65536); embs[i] = malloc(768 * sizeof(float)); } size_t doc_sizes[2] = {65536, 65536}; size_t meta_sizes[2] = {65536, 65536}; size_t emb_sizes[2] = {768, 768}; // Number of floats int result = cactus_index_get(index, ids, 2, docs, doc_sizes, metas, meta_sizes, embs, emb_sizes); if (result == 0) { printf("Retrieved documents successfully\n"); for (int i = 0; i < 2; i++) { printf("Doc %d (%zu bytes): %s\n", ids[i], doc_sizes[i], docs[i]); printf("Embedding dim: %zu floats\n", emb_sizes[i]); } }

示例:仅读取正文(元数据与向量传 NULL)

int ids[] = {1, 2, 3}; char* docs[3]; for (int i = 0; i < 3; i++) { docs[i] = malloc(65536); } size_t doc_sizes[3] = {65536, 65536, 65536}; // Pass NULL for metadata and embeddings int result = cactus_index_get(index, ids, 3, docs, doc_sizes, NULL, NULL, // No metadata NULL, NULL); // No embeddings if (result == 0) { for (int i = 0; i < 3; i++) { printf("%s\n", docs[i]); } }

3.5cactus_index_query:余弦相似度检索

int cactus_index_query( cactus_index_t index, const float** embeddings, size_t embeddings_count, size_t embedding_dim, const char* options_json, int** id_buffers, size_t* id_buffer_sizes, float** score_buffers, size_t* score_buffer_sizes );

参数:

参数必填说明
index是索引句柄
embeddings是查询向量指针数组(可一次查询多个向量),每个指针均不可为 NULL
embeddings_count是查询向量个数,必须 > 0
embedding_dim是查询向量维度,必须与索引维度一致
options_json否JSON 字符串形式的查询选项,可为 NULL(使用默认值)
id_buffers是预分配的 ID 结果缓冲区数组,每个查询一个缓冲区
id_buffer_sizes是输入为各缓冲区最大结果容量,输出为实际结果数
score_buffers是预分配的分数结果缓冲区数组
score_buffer_sizes是输入为容量,输出为实际结果数

返回值语义:0成功,结果按分数从高到低排序(相似度最高的排在最前);-1失败——若任一查询的实际命中数超过对应缓冲区容量,则整体失败且不拷贝数据(见 cactus-engine/src/index_ffi.cpp)。

Options JSON 格式:

{ "top_k": 10, "score_threshold": 0.7 }

默认值:top_k为10,score_threshold为-1.0(表示不过滤任何分数)。这两个默认值定义于 cactus-engine/src/engine.h 的QueryOptions结构体。FFI 层用简单的字符串查找解析这两个字段(见 cactus-engine/src/index_ffi.cpp),因此 JSON 中字段顺序无关紧要,但键名必须精确为"top_k"与"score_threshold"。

示例(单次批量查询两个向量):

float query1[768] = {/* ... */}; float query2[768] = {/* ... */}; const float* queries[] = {query1, query2}; // Allocate result buffers (2 queries, max 5 results each) int* ids[2]; float* scores[2]; for (int i = 0; i < 2; i++) { ids[i] = (int*)malloc(5 * sizeof(int)); scores[i] = (float*)malloc(5 * sizeof(float)); } size_t id_sizes[2] = {5, 5}; size_t score_sizes[2] = {5, 5}; int result = cactus_index_query(index, queries, 2, 768, "{\"top_k\":5}", ids, id_sizes, scores, score_sizes); if (result == 0) { for (int q = 0; q < 2; q++) { printf("Query %d: %zu results\n", q, id_sizes[q]); for (size_t i = 0; i < id_sizes[q]; i++) { printf(" ID: %d, Score: %.3f\n", ids[q][i], scores[q][i]); } } }

3.6cactus_index_compact:压缩回收空间

int cactus_index_compact(cactus_index_t index);

删除是软删除,只有调用compact才会真正回收空间。实现采用写临时文件 + 重命名替换的事务式压缩流程(见 cactus-engine/src/index.cpp):

  1. 遍历当前doc_id_map_中存活的文档,把数据拷贝到index.bin.tmp/data.bin.tmp;
  2. 将原文件重命名为.backup,临时文件重命名为正式文件,并fsync目录保证落盘;
  3. 成功后删除备份;若压缩过程中失败,会自动回滚恢复原文件。

注意:若目录中已存在.backup文件(可能源于上次中断的压缩),compact会拒绝执行并报错,这是防重复执行的保护机制。

示例:

int result = cactus_index_compact(index); if (result != 0) { fprintf(stderr, "Compaction failed: %s\n", cactus_get_last_error()); }

3.7cactus_index_destroy:释放资源

void cactus_index_destroy(cactus_index_t index);

释放索引句柄持有的全部资源:内部会munmap两个映射、关闭文件描述符(见 cactus-engine/src/index.cpp 的析构函数)。调用后建议把句柄置 NULL。

示例:

cactus_index_destroy(index); index = NULL;

四、完整实战示例

4.1 创建并填充索引

cactus_index_t index = cactus_index_init("./my_index", 768); if (!index) { fprintf(stderr, "Failed to initialize index\n"); return -1; } int ids[] = {1, 2}; const char* docs[] = { "AI is transforming technology", "Machine learning enables predictions" }; const char* metas[] = {"{\"source\":\"wiki\"}", "{\"source\":\"blog\"}"}; float emb1[768] = {0.1, 0.2, 0.3, /* ... */}; float emb2[768] = {0.4, 0.5, 0.6, /* ... */}; const float* embeddings[] = {emb1, emb2}; int result = cactus_index_add(index, ids, docs, metas, embeddings, 2, 768); if (result != 0) { fprintf(stderr, "Failed to add documents: %s\n", cactus_get_last_error()); } cactus_index_destroy(index);

4.2 相似度检索

cactus_index_t index = cactus_index_init("./my_index", 768); if (!index) { fprintf(stderr, "Failed to open index: %s\n", cactus_get_last_error()); return -1; } float query_embedding[768] = {0.1, 0.2, 0.3, /* ... */}; const float* queries[] = {query_embedding}; // Allocate buffers for 1 query with max 10 results int* ids[1]; float* scores[1]; ids[0] = (int*)malloc(10 * sizeof(int)); scores[0] = (float*)malloc(10 * sizeof(float)); size_t id_sizes[1] = {10}; size_t score_sizes[1] = {10}; int result = cactus_index_query(index, queries, 1, 768, "{\"top_k\":10,\"score_threshold\":0.7}", ids, id_sizes, scores, score_sizes); if (result == 0) { printf("Found %zu results:\n", id_sizes[0]); for (size_t i = 0; i < id_sizes[0]; i++) { printf(" ID: %d, Score: %.3f\n", ids[0][i], scores[0][i]); } } cactus_index_destroy(index);

4.3 完整 RAG 检索流程

RAG 的标准流程是:查询向量检索 → 按 ID 取回正文 → 拼装上下文喂给 LLM。Cactus Index 的两个调用(query + get)正好覆盖前两步:

cactus_index_t index = cactus_index_init("./my_index", 768); if (!index) return -1; // Search for relevant documents float query_embedding[768] = {0.1, 0.2, 0.3, /* ... */}; const float* queries[] = {query_embedding}; // Allocate query result buffers int* result_ids[1]; float* result_scores[1]; result_ids[0] = (int*)malloc(10 * sizeof(int)); result_scores[0] = (float*)malloc(10 * sizeof(float)); size_t id_sizes[1] = {10}; size_t score_sizes[1] = {10}; int query_result = cactus_index_query(index, queries, 1, 768, "{\"top_k\":3,\"score_threshold\":0.5}", result_ids, id_sizes, result_scores, score_sizes); if (query_result == 0) { // Allocate buffers for retrieving documents size_t num_results = id_sizes[0]; char* docs[10]; for (size_t i = 0; i < num_results; i++) { docs[i] = (char*)malloc(65536); } size_t doc_sizes[10]; for (size_t i = 0; i < num_results; i++) { doc_sizes[i] = 65536; } // Retrieve only documents (no metadata or embeddings needed for RAG) int get_result = cactus_index_get(index, result_ids[0], num_results, docs, doc_sizes, NULL, NULL, // No metadata NULL, NULL); // No embeddings if (get_result == 0) { // Build context from retrieved documents char context[32768] = ""; for (size_t i = 0; i < num_results; i++) { strcat(context, docs[i]); strcat(context, "\n\n"); } printf("Context: %s\n", context); } } cactus_index_destroy(index);

需要注意:score_threshold用于过滤低相似度的噪声召回;top_k决定上下文长度。设备端内存有限时,建议将context缓冲控制在合理范围(如 32KB 以内),并同步约束top_k。

4.4 删除与压缩

cactus_index_t index = cactus_index_init("./my_index", 768); if (!index) return -1; int ids[] = {1, 3, 5}; int deleted = cactus_index_delete(index, ids, 3); if (deleted == 0) { printf("Documents deleted successfully\n"); } // Compact to reclaim space int compact_result = cactus_index_compact(index); if (compact_result == 0) { printf("Index compacted successfully\n"); } cactus_index_destroy(index);

4.5 迁移 Embedding 模型(维度升级)

当把 Embedding 模型从 768 维升级到 1536 维时,无法原地修改已有索引(维度在文件头中固化),正确做法是:旧索引读出全部文档 → 用新模型重新生成向量 → 写入新索引:

// Open old index and create new index with different dimensions cactus_index_t old_index = cactus_index_init("./old_index", 768); cactus_index_t new_index = cactus_index_init("./new_index", 1536); if (!old_index || !new_index) { fprintf(stderr, "Failed to open indexes\n"); return -1; } // Get all documents from old index int all_doc_ids[] = {1, 2, 3, 4, 5}; int num_docs = 5; // Allocate buffers for old documents char* old_docs[5]; char* old_metas[5]; for (int i = 0; i < num_docs; i++) { old_docs[i] = malloc(65536); old_metas[i] = malloc(65536); } size_t doc_sizes[5], meta_sizes[5]; for (int i = 0; i < num_docs; i++) { doc_sizes[i] = 65536; meta_sizes[i] = 65536; } int get_result = cactus_index_get(old_index, all_doc_ids, num_docs, old_docs, doc_sizes, old_metas, meta_sizes, NULL, NULL); // Don't need old embeddings if (get_result == 0) { // Regenerate embeddings with new model (1536 dimensions) float new_embs[5][1536]; const float* new_emb_ptrs[5]; for (int i = 0; i < num_docs; i++) { // Generate new embedding for old_docs[i] // ... embedding generation code ... new_emb_ptrs[i] = new_embs[i]; } // Add to new index const char* doc_ptrs[5]; const char* meta_ptrs[5]; for (int i = 0; i < num_docs; i++) { doc_ptrs[i] = old_docs[i]; meta_ptrs[i] = old_metas[i]; } cactus_index_add(new_index, all_doc_ids, doc_ptrs, meta_ptrs, new_emb_ptrs, num_docs, 1536); } cactus_index_destroy(old_index); cactus_index_destroy(new_index);

读取旧索引时把embedding_buffers传 NULL,可以省去搬运旧向量的内存开销——反正旧维度向量在新索引中也用不上。

五、源码级原理剖析

5.1 写入路径:FP16 归一化与映射扩容

cactus_index_add最终调用Index::add_documents(cactus-engine/src/index.cpp)。关键步骤包括:

  1. 预校验:validate_documents检查正文/元数据长度上限、维度一致性、ID 唯一性与重复性;
  2. 扩容重映射:先ftruncate扩大两个文件,然后munmap旧映射并重新mmap新大小的区域(内存映射文件在扩容后必须重新映射才能访问新区域);
  3. 向量转换与归一化:每个向量先由cactus_fp32_to_fp16转为 FP16,再调用normalize归一化到单位长度。归一化通过cactus_matmul_f16计算内积、cactus_scalar_op_f16做开方与除法完成(cactus-engine/src/index.cpp),这些 kernel 来自 cactus-kernels;
  4. 落盘:写入完成后对两个映射执行msync(MS_SYNC)同步到磁盘。

归一化意味着查询时内积即余弦相似度,且所有文档向量被拉齐到单位球面上,分数范围集中在[-1, 1]。

5.2 查询路径:逐条扫描 + 优先队列 Top-K

Index::query(cactus-engine/src/index.cpp)采用暴力线性扫描:对每个查询向量,遍历所有存活文档计算 FP16 内积,跳过带墓碑标记(flags & 0x1)的条目,用std::priority_queue维护 Top-K 小顶堆,保证任意时刻堆中只保留分数最高的top_k个结果,最后翻转得到降序输出。这种设计在设备端数据集规模(数万到数十万条)下简单可靠、无需构建复杂索引结构;同时批量传入多个查询向量时共享一次全量扫描的访存模式。

5.3 测试验证

仓库提供了专门的索引测试套件 cactus-engine/tests/test_index.cpp,覆盖了文档中的全部核心行为,可作为验证接口语义的权威参考:

  • crud:增、查、删的基础闭环,验证删除后get返回失败;
  • batch:一次批量写入 100 条文档并随机删除 5 条,验证批量路径与删除隔离;
  • query:用目标向量检索,验证 top-1 命中正确文档、删除后不再出现在结果中;
  • compact:删除偶数 ID 后压缩,断言index.bin文件体积严格变小,且存活文档仍可读取;
  • persistence:写入 → 删除 → 压缩 → 重新打开索引,验证mmap文件的持久化与重建doc_id_map_;
  • errors:重复添加相同 ID、读取/删除不存在的 ID、重复删除、零向量添加均应失败;
  • unicode:中文与 emoji 正文(UTF-8)可完整往返;
  • constructor:用错误维度重新打开已有索引应失败(返回 NULL)。

测试通过环境变量CACTUS_INDEX_PATH指定索引根目录,且DIM = 1024,说明该 API 并不绑定文档示例中的 768 维——维度完全由调用方在init时决定。

六、多语言绑定与生态位置

Cactus Index 的 C FFI 接口已被封装进多种语言绑定,调用模式与 C 语言完全一致:

  • Kotlin:bindings/kotlin/Cactus.ios.kt 提供cactusIndexInit/cactusIndexAdd/cactusIndexDelete/cactusIndexGet/cactusIndexQuery/cactusIndexCompact/cactusIndexDestroy,底层通过cactus_index_*系列函数直接 FFI 调用;
  • Swift:bindings/swift/Cactus.swift 封装了同名方法,iOS/macOS 场景可直接使用;
  • Dart/Flutter:bindings/flutter/cactus.dart 通过dart:ffi绑定cactus_index_init、cactus_index_add、cactus_index_query、cactus_index_compact、cactus_index_destroy等符号;
  • Rust:bindings/rust/cactus.rs 声明了与 C 头文件一一对应的extern "C"函数绑定;
  • Python:python/cactus/bindings/cactus.py 与 python 提供 Python 侧的向量索引支持(文档 docs/cactus_index.md 中列出的绑定入口)。

在 Cactus 的整体架构中,Cactus Index 与 Cactus Engine API(LLM 推理、cactus_embed生成 Embedding、RAG 查询 API)配合使用,即可在设备端完成"生成向量 → 建索引 → 检索 → 拼装上下文 → 推理生成"的完整 RAG 闭环;若需要更底层的自定义张量运算,可进一步参考 Cactus Graph API。

七、最佳实践清单

  1. 检查全部返回值:所有函数均以0表示成功、-1表示失败,务必逐个检查并配合cactus_get_last_error()定位原因;
  2. 区分缓冲区单位:document_buffer_sizes与metadata_buffer_sizes的单位是字节,embedding_buffer_sizes的单位是float 个数,混淆二者是缓冲区相关错误的最常见来源;
  3. 按需取字段:cactus_index_get中不需要的字段一律传 NULL,既省内存又避免无谓拷贝;
  4. 用完即毁:任何路径上都要保证cactus_index_destroy()被调用,避免句柄泄漏;
  5. 线程模型:每个索引实例在单线程中使用(One index instance per thread),不要在多个线程间共享同一句柄并发读写;
  6. 批量写入:建议每批cactus_index_add写入 100~1000 条文档,减少ftruncate+ 重映射的系统调用次数,获得最佳吞吐;
  7. 删除后及时压缩:软删除不会释放空间,定期执行cactus_index_compact回收磁盘,但注意压缩是重操作,应在应用空闲时进行;
  8. 维度一经确定不可变:索引维度在首次创建时固化,升级 Embedding 模型请走"读旧索引 + 重新向量化 + 写入新索引"的迁移流程。

参见

  • Cactus Engine API — LLM 推理、Embedding(cactus_embed)与 RAG 查询 API
  • Cactus Graph API — 底层计算图,用于自定义张量运算
  • Cactus 引擎文档 与 快速上手
  • Python Binding — 带向量索引支持的 Python 绑定
  • Swift Binding — Swift 向量索引函数
  • Kotlin Binding — Kotlin 向量索引函数
  • Flutter Binding — Dart 向量索引函数
  • Rust Binding — Rust FFI 声明

以上内容综合自 docs/cactus_index.md 官方文档,以及 cactus-engine/src/index_ffi.cpp、cactus-engine/src/index.cpp、cactus-engine/src/engine.h、cactus-engine/cactus_engine.h 与 cactus-engine/tests/test_index.cpp 的源码实现与测试验证。

  • 推理引擎
  • 模型推理服务
  • 算子库
  • 模型量化
  • 边缘计算

【免费下载链接】cactus

Quantization, kernels, runtime and inference engine for mobiles, wearables, smart home and robots.

项目地址:https://gitcode.com/gh_mirrors/cactus12/cactus
点击查看免费下载

相关推荐

上一篇:Purpur服务器性能调优终极指南:7个核心策略实现TPS稳定20+
下一篇:3步突破Cursor Pro限制:机器标识重置与订阅绕过完整方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 9:07:39

英语写作批改工具怎么挑?2026年最新这5款别踩坑

英语写作批改这个事&#xff0c;我做了五年多&#xff0c;见过太多人踩坑了。有些老师兴冲冲买了个工具&#xff0c;用两周就扔一边&#xff0c;钱白花。也有人图便宜用免费的&#xff0c;结果批出来的东西还得自己重改一遍&#xff0c;反倒更累。选工具这件事&#xff0c;真不…

作者头像 李华
网站建设 2026/10/18 0:41:05

Panda MDX 实时样式提取:性能基准、回归门禁与复现指南

前端构建工具开发工具 【免费下载链接】panda &#x1f43c; Universal, Type-Safe, CSS-in-JS Framework for Design Systems ⚡️ 项目地址&#xff1a; https://gitcode.com/gh_mirrors/pa/panda 点击查看 免费下载 本篇技术指南围绕 Panda 仓库中 MDX 提取适配器&#xff…

作者头像 李华
网站建设 2026/10/16 2:19:50

毕业答辩PPT模板:母版、占位符与字体嵌入的工程化实践

简介&#xff1a;这份昆士兰大学答辩通用PPT模板面向即将进行毕业答辩、开题汇报或学术会议演示的高校学生与研究人员&#xff0c;帮助解决演示文稿结构松散、视觉不统一、专业度不足等问题。压缩包内共1个pptx文件&#xff0c;整体约11.61MB&#xff0c;可直接用PowerPoint或W…

作者头像 李华
网站建设 2026/10/16 13:01:45

File Signature(文件签名)

一、为什么改扩展名骗不过系统&#xff1f; 一般来说&#xff1a;文件后缀&#xff08;扩展名&#xff09;决定“谁去打开”&#xff0c;魔数&#xff08;文件签名&#xff09;决定“能不能打开”。两者不一致时&#xff0c;去打开的那个程序会发现自己读不懂文件内容&#xf…

作者头像 李华