一小时手搓轻量级可代替 Qdrant 的向量数据库

一个"向量版的 SQLite"?

前两天我直在优化goRAG那个项目,当我想RAG寻找一个存储方案时,我傻眼了:

  • Qdrant / Milvus / Weaviate?它们非常强大,但都是为了云原生和海量集群设计的。让一个本地桌面 App 启动时去跑一个几百 MB 内存占用的 Docker 容器?这绝对是反人类的设计。
  • SQLite / PostgreSQL (pgvector)?这确实是个好主意,但引入 C/C++ 依赖会让跨平台编译(尤其是 Go 的交叉编译)变成一场灾难。

所以我只能妥协,现在只能让goRAG同时支持各个主流的向量数据库,要用起来还得靠Docker,实在太不优雅了!

我真正想要的,是一个"纯粹的、无 CGO 依赖的、可以直接嵌在 Go 代码里,而且能够把数据持久化到本地单文件"的向量数据库。

我不死心在网上又收刮了一通,最后只能放弃,竟然没有人考虑要做个轻量的可嵌入式的 Qrant ?

既然找不到完全满意的轮子,那就自己造一个!我的目标很明确:

  1. 纯 Go 实现 (CGO-Free) :一个 go build 走天下。
  2. 持久化存储:程序重启,向量不丢。
  3. 支持元数据过滤 (Payload) :搜索向量时,能加上 category == "tech" 这样的硬性条件。
  4. 兼容 Qdrant API:以后如果业务做大了,代码不用改,无缝把后端 URL 切到真正的 Qdrant 集群。

2. 架构设计:拆解一个向量数据库

要在一小时内干完这件事,我们不能去造那些艰深的底层算法轮子,而是要做一位优雅的"架构整合大师"。

一个现代向量数据库,核心其实就三大块:

  1. 存储引擎 (Storage Engine):负责把向量和附带的 JSON 元数据(Payload)落盘。
  2. 索引引擎 (Index Engine):负责在海量向量中快速找到"最近邻"。
  3. 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)。

go 复制代码
// 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 返回。

go 复制代码
func (h *HNSWIndex) Search(query []float32, filter *Filter, topK int) ([]ScoredPoint, error) {
	// 如果带有过滤条件,放大召回深度以防止结果被全部过滤掉
	fetchK := topK
	if filter != nil {
		fetchK = topK * 10 
	}

	// 1. O(log N) 极速图搜索
	neighbors := h.graph.Search(query, fetchK)

	var results []ScoredPoint
	for _, neighbor := range neighbors {
		point := h.points[neighbor.Key]
		
		// 2. 精确过滤 Payload 元数据
		if !MatchFilter(point.Payload, filter) {
			continue // 作者不是 Alice,跳过!
		}

		// 3. 计算并格式化最终得分
		score := CalculateDistance(h.metric, query, point.Vector)
		results = append(results, ScoredPoint{ID: point.ID, Score: score, Payload: point.Payload})

		if len(results) == topK {
			break
		}
	}
	return results, nil
}

步骤三:暴露 Qdrant 兼容的 REST API

api/server.go 中,我们仅需几行代码就能复刻 Qdrant 的搜索接口:

go 复制代码
// 拦截 POST /collections/{name}/points/search
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
    // 解析 Qdrant 格式的请求体 (包含 vector 和 filter)
    var req struct {
        Vector []float32    `json:"vector"`
        Filter *core.Filter `json:"filter,omitempty"` 
        Limit  int          `json:"limit"`
    }
    json.NewDecoder(r.Body).Decode(&req)

    // 调用底层核心执行搜索
    results, _ := col.Search(req.Vector, req.Filter, req.Limit)

    // 返回标准 Qdrant 响应格式
    json.NewEncoder(w).Encode(map[string]interface{}{
        "status": "ok",
        "result": results,
    })
}

4. 暴力美学:性能基准测试 (Benchmark)

1小时后 .... 代码写完了,它到底有多快?

我写了一个 Benchmark 脚本,在纯内存下生成了 10,000 条 128维 的向量,并生成 1,000 条查询,对比普通的暴力轮询(Flat)和我们引入的 HNSW 图索引。

索引策略 构建耗时 平均查询延迟 吞吐量 (QPS)
Flat Index (线性扫描) 0 ms 4.267 毫秒 234 次/秒
HNSW Index (图搜索) 1688 ms 0.058 毫秒 (58微秒) 17,150 次/秒

结果极其惊艳! HNSW 带来了高达 73倍 的性能跃升。在单机单核下,轻松跑出了 1.7万 QPS ,每次检索仅需 58微秒。 想象一下,大模型生成一个词都需要几十毫秒,而你的知识库检索时间连 0.1 毫秒都不到,完全消除了 RAG 流程中的检索延迟瓶颈!


5. 成果展示:三种绝佳的食用方式

你可以通过三种方式使用我写的这个轮子:

方式一:微服务模式 (平替本地 Qdrant)

直接跑起来,它就是一个监听 18080 端口的微服务。

bash 复制代码
go run cmd/govector-server/main.go -port 18080 -db ./govector.db -hnsw=true

然后你在 Python 代码里原来请求 Qdrant 的地方,直接把端口改成 18080,一切照旧运转!

方式二:内嵌模式 (极致性能)

如果你写的是 Go 服务,连 HTTP 都不需要!直接把它像 SQLite 一样 import 进去。

go 复制代码
import "github.com/yourusername/govector/core"

// 本地落盘,零网络开销
store, _ := core.NewStorage("app.db")
col, _ := core.NewCollection("docs", 128, core.Cosine, store, true)

// 直接查!
results, _ := col.Search(queryVector, filter, 10)

方式三: home brew

macOS上只要去 brew 一下就能用

bash 复制代码
 $ brew tap DotNetAge/govector
 $ brew install govector

就这么简单!

结语

这个名为 GoVector 的项目我已经开源到了 GitHub,没有任何外部服务的依赖绑定,非常纯粹。如果国内的开发者兄弟们也在苦恼本地 AI 缺乏好用的嵌入式向量库,源码编译包都一应准备好了,欢迎来白嫖代码或者点个 Star!

👉 源码地址: github.com/DotNetAge/g...

(如果这篇文章对你有启发,欢迎在评论区交流你对本地向量存储的看法!)

相关推荐
用户6757049885024 小时前
【AI开发实战】从想法到上线,我用AI全栈开发了一款记账微信小程序
后端·aigc·ai编程
Moment4 小时前
作为前端,如果使用 Langgraph 实现第一个 Agent
前端·javascript·后端
神奇小汤圆4 小时前
高并发接口总被打崩?我用 ArrayBlockingQueue + 底层源码深度剖析搞定流控
后端
木易 士心4 小时前
MyBatis Plus 核心功能与用法
java·后端·mybatis
Victor3564 小时前
MongoDB(93)如何使用变更流跟踪数据变化?
后端
用户6757049885024 小时前
全网都在推 Claude Code,但只有这篇文章教你如何“真正”能用
后端·aigc·claude
张忠琳4 小时前
【openclaw】OpenClaw Daemon 模块超深度架构分析
ai·架构·vllm
Victor3564 小时前
MongoDB(94)什么是MongoDB Atlas?
后端
PD我是你的真爱粉4 小时前
Dify 与 LangGraph 图执行引擎原理对比:从定义层到运行时的架构拆解
人工智能·python·架构
苏三说技术4 小时前
为什么越来越多的大厂抛弃MCP,转向CLI?
后端