Zvec 是阿里巴巴开源的嵌入式(进程内)向量数据库 —— 轻量、极速,可直接嵌入应用程序。本文将介绍如何使用 Zvec 的 Go 和 Rust SDK,从零搭建一套完整的 RAG(Retrieval-Augmented Generation)系统。
署名:本文由小米 MiMo-2.5-Pro 编写,Codex-5.5 审校。
什么是 RAG?
RAG(Retrieval-Augmented Generation,检索增强生成)是一种将外部知识检索与大语言模型生成相结合的技术范式。其核心流程为:
- 索引(Indexing):将文档切分、向量化,存入向量数据库
- 检索(Retrieval):用户提问时,将问题向量化,在数据库中搜索最相关的文档片段
- 生成(Generation):将检索到的相关文档作为上下文,连同用户问题一起送入 LLM 生成回答
RAG 解决了 LLM 的几个核心痛点:
- 知识时效性:无需重新训练模型,通过更新知识库即可获取最新信息
- 幻觉问题:基于真实文档生成回答,大幅降低虚构内容的概率
- 可溯源性:回答可追溯到具体来源文档,增强可信度
为什么选择 Zvec?
在 RAG 系统中,向量数据库是核心组件。Zvec 相比其他方案有以下优势:
| 特性 | Zvec | 传统方案(Milvus/Qdrant/Weaviate) |
|---|---|---|
| 部署模式 | 进程内嵌入,无需独立服务 | 需要单独部署服务 |
| 延迟 | 毫秒级,零网络开销 | 需要网络往返 |
| 依赖 | 零外部服务依赖(仍需链接 Zvec C 库) | 需要 Docker/K8s 等 |
| 运维 | 零运维 | 需要运维团队 |
| 运行形态 | 随应用进程部署,数据本地持久化 | 依赖服务端实现 |
| 检索能力 | 向量 + 全文 + 混合检索 | 各有差异 |
对于中小型或单机优先的 RAG 系统,Zvec 的嵌入式架构意味着:
- 更简单的部署:你的 Go/Rust 服务本身就是一个向量数据库
- 更低的延迟:没有网络序列化/反序列化开销
- 更少的运维:无需管理独立的数据库集群
架构概览
┌─────────────────────────────────────────────────────────┐
│ RAG Application │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ 文档 │───▶│ Embedding │───▶│ Zvec 向量DB │ │
│ │ 加载器 │ │ 向量化模型 │ │ (进程内) │ │
│ └──────────┘ └──────────────┘ └───────┬───────┘ │
│ │ │
│ ┌──────────┐ ┌──────────────┐ │ │
│ │ LLM │◀──│ Prompt 组装 │◀───────────┘ │
│ │ 生成 │ │ 上下文拼接 │ 检索 Top-K │
│ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
核心流程:
- 文档加载 → 文本切分 → 向量化 → 存入 Zvec
- 用户提问 → 问题向量化 → Zvec 检索 → 获取相关文档
- 相关文档 + 用户问题 → Prompt 模板 → LLM 生成回答
环境准备
Zvec Go SDK
前置条件:
- Go ≥ 1.21
- C 编译器(gcc 或 clang)
- CMake ≥ 3.20 和 Ninja
# 克隆仓库(含子模块)
git clone --recursive https://github.com/zvec-ai/zvec-go.git
cd zvec-go
# 下载预编译库(推荐,无需从源码构建)
go run ./cmd/download-libs -version v0.5.0
# 或者从源码构建
make build-zvec
Zvec Rust SDK
前置条件:
- Rust 1.70+
- CMake(用于构建 C 库,可选,SDK 会自动下载预编译库)
# Cargo.toml
[dependencies]
zvec = { git = "https://github.com/zvec-ai/zvec-rust.git", tag = "v0.5.0" }
注意:首次构建时,SDK 会自动从 GitHub Releases 下载预编译的
libzvec_c_api库。如需自定义构建,可设置ZVEC_LIB_DIR环境变量。
Go 实战:构建 RAG 管道
1. 项目结构
rag-go/
├── main.go
├── embedder.go # 向量化接口
├── splitter.go # 文本切分
├── retriever.go # 检索器
├── generator.go # LLM 生成
└── go.mod
2. 定义 Zvec 集合
package rag
import (
"fmt"
zvec "github.com/zvec-ai/zvec-go"
)
// RAGCollection 封装 Zvec 集合操作
type RAGCollection struct {
collection *zvec.Collection
}
// NewRAGCollection 创建一个用于 RAG 的向量集合
// dimension: embedding 向量维度(如 OpenAI text-embedding-3-small 为 1536)
func NewRAGCollection(path string, dimension int) (*RAGCollection, error) {
// 初始化 Zvec
if err := zvec.Initialize(nil); err != nil {
return nil, err
}
// 定义 Schema
schema := zvec.NewCollectionSchema("rag_docs")
defer schema.Destroy()
// 文档 ID 字段(带倒排索引,支持精确查找)
idField := zvec.NewFieldSchema("doc_id", zvec.DataTypeString, false, 0)
defer idField.Destroy()
invertParams, _ := zvec.NewInvertIndexParams(true, false)
defer invertParams.Destroy()
idField.SetIndexParams(invertParams)
schema.AddField(idField)
// 文档内容字段
contentField := zvec.NewFieldSchema("content", zvec.DataTypeString, false, 0)
defer contentField.Destroy()
schema.AddField(contentField)
// 来源元数据字段
sourceField := zvec.NewFieldSchema("source", zvec.DataTypeString, true, 0)
defer sourceField.Destroy()
schema.AddField(sourceField)
// 向量字段(HNSW 索引,余弦相似度)
embField := zvec.NewFieldSchema("embedding", zvec.DataTypeVectorFP32, false, dimension)
defer embField.Destroy()
hnswParams, _ := zvec.NewHNSWIndexParams(zvec.MetricTypeCosine, 16, 200)
defer hnswParams.Destroy()
embField.SetIndexParams(hnswParams)
schema.AddField(embField)
// 创建并打开集合
collection, err := zvec.CreateAndOpen(path, schema, nil)
if err != nil {
_ = zvec.Shutdown()
return nil, err
}
return &RAGCollection{
collection: collection,
}, nil
}
func (r *RAGCollection) Close() error {
if r.collection != nil {
if err := r.collection.Close(); err != nil {
return err
}
}
return zvec.Shutdown()
}
3. 文档索引(写入)
// Document 表示一个待索引的文档片段
type Document struct {
ID string
Content string
Source string
}
// IndexDocuments 批量索引文档片段
func (r *RAGCollection) IndexDocuments(docs []Document, embedder EmbedFunc) error {
zvecDocs := make([]*zvec.Doc, 0, len(docs))
for _, doc := range docs {
// 将文档内容向量化
embedding, err := embedder(doc.Content)
if err != nil {
return fmt.Errorf("embed doc %s: %w", doc.ID, err)
}
// 构建 Zvec 文档
zdoc := zvec.NewDoc()
zdoc.SetPK(doc.ID)
zdoc.AddStringField("doc_id", doc.ID)
zdoc.AddStringField("content", doc.Content)
zdoc.AddStringField("source", doc.Source)
zdoc.AddVectorFP32Field("embedding", embedding)
zvecDocs = append(zvecDocs, zdoc)
}
// 释放文档资源
defer func() {
for _, doc := range zvecDocs {
doc.Destroy()
}
}()
// 批量插入
if _, err := r.collection.Insert(zvecDocs); err != nil {
return err
}
// 刷新到磁盘
return r.collection.Flush()
}
// EmbedFunc 向量化函数类型
type EmbedFunc func(text string) ([]float32, error)
4. 检索(查询)
// SearchResult 检索结果
type SearchResult struct {
DocID string
Content string
Source string
Score float32
}
// Retrieve 检索与 query 最相关的 topK 个文档
func (r *RAGCollection) Retrieve(queryEmbedding []float32, topK int) ([]SearchResult, error) {
query := zvec.NewSearchQuery()
query.SetFieldName("embedding")
query.SetQueryVector(queryEmbedding)
query.SetTopK(topK)
query.SetOutputFields([]string{"doc_id", "content", "source"})
results, err := r.collection.Query(query)
query.Destroy()
if err != nil {
return nil, err
}
defer zvec.FreeDocs(results)
searchResults := make([]SearchResult, 0, len(results))
for _, r := range results {
sr := SearchResult{
DocID: r.GetPK(),
Score: r.GetScore(),
}
// 读取字段值(简化示意,实际需要根据 SDK API 获取字段)
searchResults = append(searchResults, sr)
}
return searchResults, nil
}
5. 完整 RAG 管道
package main
import (
"fmt"
"log"
"strings"
rag "your-project/rag"
)
func main() {
// 1. 创建向量集合
collection, err := rag.NewRAGCollection("./rag_data", 1536)
if err != nil {
log.Fatal(err)
}
defer collection.Close()
// 2. 准备文档(示例:知识库文档片段)
docs := []rag.Document{
{
ID: "doc_001",
Content: "Zvec 是阿里巴巴开源的嵌入式向量数据库,支持 HNSW、IVF 等多种索引类型...",
Source: "zvec_readme.md",
},
{
ID: "doc_002",
Content: "RAG 系统通过检索外部知识来增强 LLM 的生成能力,减少幻觉问题...",
Source: "rag_introduction.md",
},
// ... 更多文档
}
// 3. 索引文档
embedder := createEmbedder() // 调用 OpenAI / 本地模型的向量化接口
if err := collection.IndexDocuments(docs, embedder); err != nil {
log.Fatal(err)
}
// 4. RAG 查询
question := "Zvec 支持哪些索引类型?"
qEmbedding, _ := embedder(question)
results, _ := collection.Retrieve(qEmbedding, 5)
// 5. 组装 Prompt 并调用 LLM
context := buildContext(results)
prompt := fmt.Sprintf(`基于以下参考资料回答问题。
参考资料:
%s
问题:%s
请用中文回答,并注明信息来源。`, context, question)
answer, _ := callLLM(prompt)
fmt.Println(answer)
}
func buildContext(results []rag.SearchResult) string {
var parts []string
for i, r := range results {
parts = append(parts, fmt.Sprintf(
"[%d] (来源: %s, 相关度: %.4f)\n%s",
i+1, r.Source, r.Score, r.Content,
))
}
return strings.Join(parts, "\n\n")
}
Go 示例中的 createEmbedder 和 callLLM 需要按你实际使用的 embedding 模型和 LLM API 自行实现,不属于 Zvec SDK。
Rust 实战:构建 RAG 管道
1. 项目结构
rag-rust/
├── Cargo.toml
├── src/
│ ├── main.rs
│ ├── collection.rs # Zvec 集合封装
│ ├── embedder.rs # 向量化接口
│ ├── retriever.rs # 检索器
│ └── generator.rs # LLM 生成
2. Cargo.toml
[package]
name = "rag-rust"
version = "0.1.0"
edition = "2021"
[dependencies]
zvec = { git = "https://github.com/zvec-ai/zvec-rust.git", tag = "v0.5.0" }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
3. 定义集合与索引
use zvec::*;
use anyhow::Result;
pub struct RagCollection {
collection: Collection,
}
/// 创建 RAG 用的向量集合
pub fn create_rag_collection(path: &str, dimension: u32) -> Result<RagCollection> {
// 初始化引擎
initialize(None)?;
// 定义 Schema
let schema = CollectionSchema::builder("rag_docs")
// 文档 ID 字段
.add_field(FieldSchema::new("doc_id", DataType::String, false, 0)?)
// 文档内容字段
.add_field(FieldSchema::new("content", DataType::String, false, 0)?)
// 来源元数据
.add_field(FieldSchema::new("source", DataType::String, true, 0)?)
// 向量字段(HNSW 索引 + 余弦相似度)
.add_vector_field(
"embedding",
DataType::VectorFp32,
dimension,
IndexParams::hnsw(MetricType::Cosine, 16, 200)?,
)
.build()?;
// 创建并打开集合
let collection = Collection::create_and_open(path, &schema, None)?;
Ok(RagCollection { collection })
}
4. 文档索引
/// 文档片段
pub struct Document {
pub id: String,
pub content: String,
pub source: String,
}
impl RagCollection {
/// 批量索引文档
pub fn index_documents(
&self,
docs: &[Document],
embedder: &dyn Fn(&str) -> Result<Vec<f32>>,
) -> Result<()> {
let mut zvec_docs = Vec::new();
for doc in docs {
let embedding = embedder(&doc.content)?;
let mut zdoc = Doc::new()?;
zdoc.set_pk(&doc.id);
zdoc.add_string("doc_id", &doc.id)?;
zdoc.add_string("content", &doc.content)?;
zdoc.add_string("source", &doc.source)?;
zdoc.add_vector_f32("embedding", &embedding)?;
zvec_docs.push(zdoc);
}
// 批量插入(借用引用)
let doc_refs: Vec<&Doc> = zvec_docs.iter().collect();
self.collection.insert(&doc_refs)?;
// 刷新到磁盘
self.collection.flush()?;
Ok(())
}
/// 检索最相关的文档
pub fn retrieve(&self, query_embedding: &[f32], top_k: i32) -> Result<Vec<SearchResult>> {
let query = SearchQuery::builder()
.field_name("embedding")
.vector(query_embedding)
.topk(top_k)
.output_fields(&["doc_id", "content", "source"])
.build()?;
let results = self.collection.query(&query)?;
let search_results: Vec<SearchResult> = results
.iter()
.map(|r| SearchResult {
doc_id: r.get_pk().unwrap_or_default().to_string(),
score: r.get_score(),
// 实际字段读取需根据 SDK API 获取
content: String::new(),
source: String::new(),
})
.collect();
Ok(search_results)
}
}
#[derive(Debug)]
pub struct SearchResult {
pub doc_id: String,
pub content: String,
pub source: String,
pub score: f32,
}
5. 完整 RAG 管道
use anyhow::Result;
mod collection;
mod embedder;
#[tokio::main]
async fn main() -> Result<()> {
// 1. 创建向量集合
let rag = collection::create_rag_collection("./rag_data", 1536)?;
// 2. 准备文档
let docs = vec![
collection::Document {
id: "doc_001".into(),
content: "Zvec 是阿里巴巴开源的嵌入式向量数据库,支持 HNSW、IVF 等多种索引类型...".into(),
source: "zvec_readme.md".into(),
},
collection::Document {
id: "doc_002".into(),
content: "RAG 系统通过检索外部知识来增强 LLM 的生成能力,减少幻觉问题...".into(),
source: "rag_introduction.md".into(),
},
];
// 3. 索引文档
let embedder = embedder::create_embedder();
rag.index_documents(&docs, &embedder)?;
// 4. RAG 查询
let question = "Zvec 支持哪些索引类型?";
let q_embedding = embedder(question)?;
let results = rag.retrieve(&q_embedding, 5)?;
// 5. 组装 Prompt 并调用 LLM
let context: String = results
.iter()
.enumerate()
.map(|(i, r)| {
format!(
"[{}] (来源: {}, 相关度: {:.4})\n{}",
i + 1, r.source, r.score, r.content
)
})
.collect::<Vec<_>>()
.join("\n\n");
let prompt = format!(
"基于以下参考资料回答问题。\n\n参考资料:\n{}\n\n问题:{}\n\n请用中文回答,并注明信息来源。",
context, question
);
let answer = call_llm(&prompt).await?;
println!("{}", answer);
Ok(())
}
Rust 示例中的 create_embedder 和 call_llm 同样需要按你实际使用的 embedding 模型和 LLM API 自行实现,不属于 Zvec SDK。
6. Rust 中的资源管理优势
Rust SDK 采用 RAII(资源获取即初始化)模式,所有 C 资源通过 Drop trait 自动释放:
{
let doc = Doc::new()?;
doc.set_pk("test");
doc.add_vector_f32("embedding", &[0.1, 0.2, 0.3])?;
// ... 使用 doc
} // <-- doc 在此处自动释放,无需手动 Destroy()
配合 Rust 的所有权系统,可以有效避免资源泄漏和悬垂指针问题,这在长时间运行的 RAG 服务中尤为重要。
混合检索:向量 + 全文搜索
Zvec v0.5.0 引入了原生全文检索(FTS)能力,可以在 RAG 系统中实现混合检索——同时利用语义相似度和关键词精确匹配,显著提升检索质量。
为什么需要混合检索?
| 查询类型 | 纯向量检索 | 纯关键词检索 | 混合检索 |
|---|---|---|---|
| “什么是深度学习?” | ✅ 语义匹配好 | ⚠️ 关键词不精确 | ✅✅ |
| “error code 0x80070005” | ⚠️ 语义相近但不精确 | ✅ 精确匹配 | ✅✅ |
| “如何优化数据库查询性能” | ✅ 语义好 | ⚠️ 可能漏掉同义词 | ✅✅ |
Go 中的混合检索
需要注意:在 zvec-go v0.5.0 中,FTS 查询通过 FTS 载荷挂到 SearchQuery.SetFTS 上;SubQuery 暴露的是向量/稀疏向量检索相关方法,并没有 SetMatchString 或 SetTopK。因此 Go 侧更稳妥的写法是先分别执行向量检索和全文检索,再在应用层做 RRF 或加权融合。
// 创建 FTS 索引参数
ftsParams, _ := zvec.NewFTSIndexParams("default", nil, "")
defer ftsParams.Destroy()
contentField := zvec.NewFieldSchema("content", zvec.DataTypeString, false, 0)
defer contentField.Destroy()
contentField.SetIndexParams(ftsParams)
schema.AddField(contentField)
// 第一路:向量检索
vectorQuery := zvec.NewSearchQuery()
defer vectorQuery.Destroy()
vectorQuery.SetFieldName("embedding")
vectorQuery.SetQueryVector(queryEmbedding)
vectorQuery.SetTopK(20)
vectorQuery.SetOutputFields([]string{"doc_id", "content", "source"})
vectorResults, err := collection.Query(vectorQuery)
if err != nil {
return err
}
defer zvec.FreeDocs(vectorResults)
// 第二路:全文检索
ftsQuery := zvec.NewSearchQuery()
defer ftsQuery.Destroy()
ftsQuery.SetFieldName("content")
ftsQuery.SetQueryVector(queryEmbedding) // v0.5.0 官方示例仍会设置 query vector
ftsQuery.SetTopK(20)
ftsQuery.SetOutputFields([]string{"doc_id", "content", "source"})
fts := zvec.NewFTS()
fts.SetMatchString("Zvec 索引类型")
ftsQuery.SetFTS(fts)
fts.Destroy()
ftsResults, err := collection.Query(ftsQuery)
if err != nil {
return err
}
defer zvec.FreeDocs(ftsResults)
// 应用层融合:按 doc_id 对 vectorResults 与 ftsResults 做 RRF 或加权合并
results := rrfFuse(vectorResults, ftsResults, 60)
Rust 中的混合检索
Rust SDK v0.5.0 的接口形态也类似:FTS 通过 SearchQueryBuilder::fts_match_string 或 Fts 载荷挂到 SearchQuery 上;MultiQuery 当前面向多个向量/稀疏向量 SubQuery,不是下面这种 FTS 子查询 builder。
// 创建带 FTS 索引的 Schema
let schema = CollectionSchema::builder("rag_docs")
.add_indexed_field(
"content",
DataType::String,
IndexParams::fts(Some("default"), None, None)?,
)
.add_vector_field(
"embedding",
DataType::VectorFp32,
1536,
IndexParams::hnsw(MetricType::Cosine, 16, 200)?,
)
.build()?;
// 第一路:向量检索
let vector_query = SearchQuery::builder()
.field_name("embedding")
.vector(&query_embedding)
.topk(20)
.output_fields(&["doc_id", "content", "source"])
.build()?;
let vector_results = collection.query(&vector_query)?;
// 第二路:全文检索
let fts_query = SearchQuery::builder()
.field_name("content")
.vector(&query_embedding)
.topk(20)
.output_fields(&["doc_id", "content", "source"])
.fts_match_string("Zvec 索引类型")
.build()?;
let fts_results = collection.query(&fts_query)?;
// 应用层融合:按 doc_id 对两路结果做 RRF 或加权合并
let results = rrf_fuse(&vector_results, &fts_results, 60);
这里的 rrfFuse / rrf_fuse 是应用层融合函数,需要按文档 ID 去重并按 RRF 分数重新排序。
融合策略说明
Zvec 支持两种融合策略:
- RRF(Reciprocal Rank Fusion):基于排名的融合,适合大多数场景。
rankConstant通常设为 60。 - Weighted(加权融合):为每个子查询分配权重,适合你明确知道各路检索重要性的场景。
性能优化建议
1. 索引类型选择
| 数据规模 | 推荐索引 | 说明 |
|---|---|---|
| < 10 万条 | Flat | 暴力搜索,100% 准确率 |
| 10 万 ~ 1000 万 | HNSW | 图索引,速度与精度的最佳平衡 |
| > 1000 万 | HNSW + 量化 | 优先控制内存占用,并按实际数据分布压测 |
// HNSW + FP16 量化(内存减半,精度损失极小)
hnswParams, _ := zvec.NewHNSWIndexParams(zvec.MetricTypeCosine, 16, 200)
_ = hnswParams.SetQuantizeType(zvec.QuantizeTypeFP16)
2. 批量操作
// ❌ 逐条插入(慢)
for doc in docs {
collection.insert(&[&doc])?;
}
// ✅ 批量插入(快)
let doc_refs: Vec<&Doc> = docs.iter().collect();
collection.insert(&doc_refs)?;
3. 文本切分策略
在 RAG 中,文档切分质量直接影响检索效果:
// 推荐切分参数
type SplitConfig struct {
ChunkSize int // 每个片段的 token 数(建议 256-512)
ChunkOverlap int // 片段间的重叠 token 数(建议 50-100)
SplitBy string // 切分单位:"sentence" | "paragraph" | "token"
}
4. Top-K 与重排序
// 两阶段检索:先粗检索更多候选,再精排
coarseResults, _ := collection.Retrieve(queryEmbedding, 50) // 粗检索 Top-50
fineResults := reranker.Rerank(question, coarseResults, 5) // 精排取 Top-5
完整示例项目结构
Go 项目
rag-go/
├── go.mod
├── go.sum
├── main.go # 入口,编排 RAG 流程
├── config.go # 配置管理
├── splitter/
│ ├── splitter.go # 文本切分器
│ └── splitter_test.go
├── embedder/
│ ├── openai.go # OpenAI Embedding API
│ ├── local.go # 本地模型(如 ONNX Runtime)
│ └── embedder.go # 接口定义
├── store/
│ └── zvec_store.go # Zvec 向量存储封装
├── retriever/
│ └── retriever.go # 检索器(支持混合检索)
├── generator/
│ ├── llm.go # LLM 调用接口
│ ├── openai.go # OpenAI 实现
│ └── prompt.go # Prompt 模板
└── loader/
├── pdf.go # PDF 加载器
├── markdown.go # Markdown 加载器
└── loader.go # 接口定义
Rust 项目
rag-rust/
├── Cargo.toml
├── src/
│ ├── main.rs
│ ├── config.rs
│ ├── splitter/
│ │ ├── mod.rs
│ │ └── strategies.rs
│ ├── embedder/
│ │ ├── mod.rs
│ │ ├── openai.rs
│ │ └── local.rs
│ ├── store/
│ │ └── zvec_store.rs
│ ├── retriever/
│ │ └── mod.rs
│ ├── generator/
│ │ ├── mod.rs
│ │ ├── openai.rs
│ │ └── prompt.rs
│ └── loader/
│ ├── mod.rs
│ ├── pdf.rs
│ └── markdown.rs
└── tests/
└── integration_test.rs
总结
Zvec 作为一款嵌入式向量数据库,为构建 RAG 系统提供了独特的优势:
- 零部署成本:无需独立的数据库服务,直接嵌入你的 Go/Rust 应用
- 极致性能:进程内调用,毫秒级检索延迟
- 混合检索:支持向量 + 全文检索,可通过两路检索和 RRF/加权融合提升 RAG 检索质量
- 资源模型清晰:进程内调用,集合关闭、
Flush、SDKShutdown都可以在应用生命周期中显式管理 - 多语言支持:Go(cgo 绑定)和 Rust(FFI 绑定)SDK 均提供完整的类型安全 API
对于需要快速搭建、低运维成本的 RAG 系统,Zvec 是一个非常值得考虑的选择。
参考链接
本文基于 Zvec Go/Rust SDK v0.5.0 编写。