一小时手搓轻量级向量数据库,能否替代Qdrant?
摘要:在 AI 应用爆发的今天,我们在做桌面端或边缘端 RAG(检索增强生成)应用时,真的需要动辄部署一套分布式的 Qdrant 或 Milvus 吗?本文将带你从 0 到 1,用纯 Go 语言在**一小时内**手写一个内嵌级的轻量化向量数据库
一个“向量版的 SQLite”?
前两天我直在优化goRAG那个项目,当我想RAG寻找一个存储方案时,我傻眼了:
用 Qdrant / Milvus / Weaviate?它们非常强大,但都是为了云原生和海量集群设计的。让一个本地桌面 App 启动时去跑一个几百 MB 内存占用的 Docker 容器?这绝对是反人类的设计。
用 SQLite / PostgreSQL (pgvector)?这确实是个好主意,但引入 C/C++ 依赖会让跨平台编译(尤其是 Go 的交叉编译)变成一场灾难。
所以我只能妥协,现在只能让goRAG同时支持各个主流的向量数据库,要用起来还得靠Docker,实在太不优雅了!
我真正想要的,是一个“纯粹的、无 CGO 依赖的、可以直接嵌在 Go 代码里,而且能够把数据持久化到本地单文件”的向量数据库。
我不死心在网上又收刮了一通,最后只能放弃,竟然没有人考虑要做个轻量的可嵌入式的 Qrant ?
既然找不到完全满意的轮子,那就自己造一个!我的目标很明确:
纯 Go 实现 (CGO-Free):一个 go build 走天下。
持久化存储:程序重启,向量不丢。
支持元数据过滤 (Payload):搜索向量时,能加上 category == "tech" 这样的硬性条件。
兼容 Qdrant API:以后如果业务做大了,代码不用改,无缝把后端 URL 切到真正的 Qdrant 集群。
2. 架构设计:拆解一个向量数据库
要在一小时内干完这件事,我们不能去造那些艰深的底层算法轮子,而是要做一位优雅的“架构整合大师”。
一个现代向量数据库,核心其实就三大块:
存储引擎 (Storage Engine):负责把向量和附带的 JSON 元数据(Payload)落盘。
索引引擎 (Index Engine):负责在海量向量中快速找到“最近邻”。
API 层 (Serving Layer):负责对外提供通信接口。
技术选型:
原则:必须小!
存储引擎:我选择了 go.etcd.io/bbolt (BoltDB)。它是一个纯 Go 写的 KV 数据库,极其轻量,性能极佳,数据全保存在本地一个 .db 文件里,简直是嵌入式存储的完美之选。
索引引擎:暴力轮询(Flat Index)在几百条数据时还凑合,上万条就拉胯了。我们需要 HNSW(分层可导航小世界) 算法。我在这里引入了 Coder 团队开源的纯 Go HNSW 库 github.com/coder/hnsw。
API 层:直接使用 Go 1.22 原生的 http.ServeMux 增强路由,零框架依赖。
3. 核心代码实战:如何将它们组装起来?
步骤一:定义数据模型 (对齐 Qdrant)
为了能兼容 Qdrant,我们的数据结构必须和它长得一样:一个点(Point)包含 ID、向量(Vector)以及元数据(Payload)。
// Payload 模拟 Qdrant 的 JSON 元数据
type Payload map[string]interface{}
type PointStruct struct {
ID string `json:"id"`
Vector []float32 `json:"vector"`
Payload Payload `json:"payload,omitempty"` // 用于条件过滤
}
步骤二:实现带 Payload 过滤的 HNSW 搜索
这是最核心的部分。单纯的 HNSW 只能做空间上的最近邻搜索,但如果用户要求“帮我找最相似的 10 篇文章,但作者必须是 Alice”怎么办?
我们的解法是 后置过滤 (Post-Filtering):
当带有 Filter 时,我们让 HNSW 引擎超额召回 (Over-fetch)(比如取 Top 100),然后在内存中进行精准的 Payload 匹配,剔除不符合条件的结果,最后再截取 Top 10 返回。
