- 推理引擎
- 模型推理服务
- 算子库
- 模型量化
- 边缘计算
【免费下载链接】cactus
Quantization, kernels, runtime and inference engine for mobiles, wearables, smart home and robots.
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.bin | Embedding 向量(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_dim | Embedding 维度;已有索引必须与该维度一致 |
返回:成功返回索引句柄,失败返回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_count | ID 数量,必须 > 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):
- 遍历当前
doc_id_map_中存活的文档,把数据拷贝到index.bin.tmp/data.bin.tmp; - 将原文件重命名为
.backup,临时文件重命名为正式文件,并fsync目录保证落盘; - 成功后删除备份;若压缩过程中失败,会自动回滚恢复原文件。
注意:若目录中已存在.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)。关键步骤包括:
- 预校验:
validate_documents检查正文/元数据长度上限、维度一致性、ID 唯一性与重复性; - 扩容重映射:先
ftruncate扩大两个文件,然后munmap旧映射并重新mmap新大小的区域(内存映射文件在扩容后必须重新映射才能访问新区域); - 向量转换与归一化:每个向量先由
cactus_fp32_to_fp16转为 FP16,再调用normalize归一化到单位长度。归一化通过cactus_matmul_f16计算内积、cactus_scalar_op_f16做开方与除法完成(cactus-engine/src/index.cpp),这些 kernel 来自 cactus-kernels; - 落盘:写入完成后对两个映射执行
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。
七、最佳实践清单
- 检查全部返回值:所有函数均以
0表示成功、-1表示失败,务必逐个检查并配合cactus_get_last_error()定位原因; - 区分缓冲区单位:
document_buffer_sizes与metadata_buffer_sizes的单位是字节,embedding_buffer_sizes的单位是float 个数,混淆二者是缓冲区相关错误的最常见来源; - 按需取字段:
cactus_index_get中不需要的字段一律传 NULL,既省内存又避免无谓拷贝; - 用完即毁:任何路径上都要保证
cactus_index_destroy()被调用,避免句柄泄漏; - 线程模型:每个索引实例在单线程中使用(One index instance per thread),不要在多个线程间共享同一句柄并发读写;
- 批量写入:建议每批
cactus_index_add写入 100~1000 条文档,减少ftruncate+ 重映射的系统调用次数,获得最佳吞吐; - 删除后及时压缩:软删除不会释放空间,定期执行
cactus_index_compact回收磁盘,但注意压缩是重操作,应在应用空闲时进行; - 维度一经确定不可变:索引维度在首次创建时固化,升级 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.
相关推荐
ZincSearch 索引映射(Index Mapping)API 实战:`/api/:target/_mapping` 端点详解与源码测试解析
ZincSearch 索引映射(Index Mapping)API 实战: /api/:target/_mapping 端点详解与源码测试解析 导读 本文围绕
搜索引擎后端全文检索Feast 向量数据库集成:基于 ADR-0010 的 RAG 检索能力设计、实现与实战指南
Feast 向量数据库集成:基于 ADR 0010 的 RAG 检索能力设计、实现与实战指南 本文基于 Feast 仓库中的架构决策记录 ADR 0010: V
MLOps后端数据工程RAG 与向量数据库实战:构建基于检索增强生成的 LLM 应用——基于 generative-ai-for-beginners 第 15 课
RAG 与向量数据库实战:构建基于检索增强生成的 LLM 应用——基于 generative ai for beginners 第 15 课 本文围绕 gene
教程人工智能大模型
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考