# 使用 Go 构建本地向量检索系统:Go + Qdrant 实战指南
## 背景介绍
向量检索是 AI 应用开发中的核心技术。传统的关键词匹配只能做精确匹配,而向量检索通过将文本转换为高维向量,可以在语义空间中找到最相似的内容。这意味着即使用户搜索的 Query 与存储的文档没有共同的词语,只要语义相近,就能被检索到。
Qdrant 是一个用 Rust 开发的开源向量数据库,支持高效的向量相似度搜索。它提供本地单机模式,无需复杂的部署,非常适合个人开发者学习和小型项目使用。与云端向量数据库服务相比,Qdrant 本地模式完全免费,且数据完全存储在本地,隐私性更有保障。
Go 语言以其高效的并发模型和简洁的部署方式著称,是构建 AI 工具和后端服务的理想选择。本文将详细介绍如何使用 Go 语言连接 Qdrant 本地服务,实现向量数据的存储和语义检索,帮助开发者快速搭建本地化的 RAG 检索系统。
## 问题描述
在实际 AI 应用开发中,很多开发者会遇到以下困惑:如何将文本转换为向量、如何存储这些向量、如何快速检索相似内容、Go 语言能否很好地支持这些操作。
传统的方案需要依赖 Python 环境,借助 langchain、sentence-transformers 等库实现。但对于熟悉 Go 语言的开发者,维护额外的 Python 环境增加了复杂度。有没有纯 Go 方案的答案是肯定的。
本文要解决的问题是:使用 Go 语言本地运行 Qdrant,将文本向量化后存储,并实现语义检索功能。整个过程不依赖 Python 环境,部署简单,适合个人开发者快速验证想法。
## 详细步骤
### 步骤一:安装 Qdrant 本地服务
Qdrant 提供多种安装方式。对于快速验证,使用 Docker 是最简单的方式。如果你的机器上安装了 Docker,执行以下命令即可启动 Qdrant 本地服务:
“`bash
docker run -d –name qdrant -p 6333:6333 -p 6334:6334 -v qdrant_storage:/qdrant/storage qdrant/qdrant
“`
这条命令做了几件事:后台启动名为 qdrant 的容器,将内部端口 6333 和 6334 映射到宿主机,创建名为 qdrant_storage 的卷用于持久化存储数据。容器启动后,Qdrant 的 REST API 监听在 6333 端口,gRPC 接口在 6334 端口。
如果不想使用 Docker,Qdrant 也提供预编译的二进制文件,可以从 GitHub Releases 页面下载对应平台的版本,解压后直接运行。这种方式同样简单,只是需要手动管理进程。
服务启动后,可以访问 http://localhost:6333/dashboard 查看 Web UI,验证服务是否正常运行。Qdrant 的 Dashboard 提供了查询界面和基本的监控功能,对于调试非常有帮助。
### 步骤二:安装 Go Qdrant 客户端
Go 语言的 Qdrant 客户端已经成熟稳定,可以直接使用 go get 安装。在项目目录下执行以下命令:
“`bash
go get github.com/qdrant/go-client/qdrant
“`
客户端提供了完整的 CRUD 操作支持,包括向量集合的创建、向量数据的插入、相似度搜索等功能。客户端的 API 设计比较直观,有 Go 开发经验的开发者应该能快速上手。
如果之前没有创建过 Go 项目,需要先初始化项目结构。执行 go mod init 生成 go.mod 文件,然后上述命令会正确添加依赖。��赖管理在 Go 1.17 版本之后完全自动化,不需要手动维护文件。
### 步骤三:文本向量化处理
文本向量化是向量检索的关键环节。常用的方案有几种:第一种是调用 OpenAI 的 embedding 接口,需要 API Key 且有调用成本限制;第二种是使用开源的 embedding 模型,需要本地运行模型推理;第三种是使用预计算的向量,直接存入数据库。
对于学习和快速验证场景,使用现成的文本嵌入服务更加简单。HuggingFace 提供了免费的 embedding API,虽然有速率限制但足够开发测试使用。
以下是一个简单的 embedding 请求示例,使用 HTTP 调用 HuggingFace 的 sentence-transformers 模型:
“`go
func getEmbedding(text string) ([]float32, error) {
client := http.Client{}
req, _ := http.NewRequest(“POST”,
“https://api-inference.huggingface.co/pipeline/sentence-transformers/paraphrase-MiniLM-L6-v2”,
strings.NewReader(`{“inputs”: “`+text+`”}`))
req.Header.Set(“Authorization”, “Bearer “+os.Getenv(“HF_TOKEN”))
req.Header.Set(“Content-Type”, “application/json”)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result [][]float32
json.NewDecoder(resp.Body).Decode(&result)
return result[0], nil
}
“`
这段代码创建 HTTP POST 请求,调用 HuggingFace 的 paraphrase-MiniLM-L6-v2 模型,将文本转换为 384 维的向量。模型名称中的 MiniLM 表示轻量级,L6 表示 6 层网络,v2 是版本号。
需要注意的是,实际使用中需要申请 HuggingFace 的 API Token 并设置为环境变量。对于生产环境,建议使用本地部署的 embedding 模型或者调用付费服务。
### 步骤四:代码实现完整示例
以下是完整的 Go 代码,实现向量数据的存储和检索:
“`go
package main
import (
“context”
“fmt”
“os”
“strings”
“net/http”
“encoding/json”
qdrant “github.com/qdrant/go-client/qdrant”
“google.golang.org/grpc”
“google.golang.org/grpc/credentials/insecure”
)
const CollectionName = “my_documents”
type EmbeddingResponse [][]float32
func getEmbedding(text string) ([]float32, error) {
payload := fmt.Sprintf(`{“inputs”: “%s”}`, text)
req, _ := http.NewRequest(“POST”,
“https://api-inference.huggingface.co/pipeline/sentence-transformers/paraphrase-MiniLM-L6-v2”,
strings.NewReader(payload))
req.Header.Set(“Authorization”, “Bearer “+os.Getenv(“HF_TOKEN”))
req.Header.Set(“Content-Type”, “application/json”)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result EmbeddingResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result[0], nil
}
func main() {
// 连接 Qdrant 服务
conn, err := grpc.Dial(“localhost:6334”, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic(err)
}
client := qdrant.NewQdrantClient(conn)
ctx := context.Background()
// 创建向量集合
client.CreateCollection(ctx, &qdrant.CreateCollection{
CollectionName: CollectionName,
VectorsConfig: &qdrant.VectorsConfig{
Config: &qdrant.VectorsConfig_Params{
Params: &qdrant.VectorParams{
Size: 384,
Distance: qdrant.Distance_Cosine,
},
},
},
})
// 准备测试数据
documents := []struct {
ID string
Content string
}{
{“1”, “如何学习 Go 语言的并发编程”},
{“2”, “Python 的 asyncio 异步编程教程”},
{“3”, “Rust 语言所有权系统详解”},
{“4”, “JavaScript 前端开发最佳实践”},
{“5”, “Kubernetes 容器编排平台搭建”},
}
// 向量化并存储文档
for _, doc := range documents {
vec, err := getEmbedding(doc.Content)
if err != nil {
fmt.Printf(“获取向量失败: %v\n”, err)
continue
}
client.Upsert(ctx, &qdrant.UpsertPoints{
CollectionName: CollectionName,
Points: []*qdrant.PointStruct{
{
Id: &qdrant.PointId{Identifier: &qdrant.PointId_Num{Num: doc.ID}},
Vectors: &qdrant.Vectors{Vector: []*qdrant.Vector{{Data: vec}}},
Payload: &qdrant.Value{
Kind: &qdrant.Value_StructValue{
StructValue: &qdrant.Struct{
Fields: map[string]*qdrant.Value{
“content”: {Kind: &qdrant.Value_StringValue{StringValue: doc.Content}},
},
},
},
},
},
},
})
fmt.Printf(“已存储文档: %s\n”, doc.Content)
}
// 执行语义检索
query := “Go 语言并发处理”
queryVec, err := getEmbedding(query)
if err != nil {
panic(err)
}
searchResult, err := client.Search(ctx, &qdrant.SearchPoints{
CollectionName: CollectionName,
Vector: queryVec,
Limit: 3,
WithPayload: true,
})
if err != nil {
panic(err)
}
fmt.Printf(“\n搜索Query: %s\n”, query)
fmt.Println(“搜索结果:”)
for _, result := range searchResult.Result {
payload := result.Payload.GetStructValue().Fields
fmt.Printf(“- ID: %s, 内容: %s, 得分: %.4f\n”,
result.GetId().GetNum(),
payload[“content”].GetStringValue(),
result.GetScore())
}
}
“`
代码说明:程序首先建立与 Qdrant 的 gRPC 连接,然后创建名为 my_documents 的向量集合,设置向量维度为 384(匹配 MiniLM 模型的输出维度),距离度量方式为余弦相似度。
接下来准备了一组测试文档,涵盖不同技术主题。程序遍历这些文档,调用 embedding 接口将文本转换为向量,然后使用 Upsert 操作存入 Qdrant。
最后,用户输入查询”Go 语言并发处理”,程序将其向量化后执行 Search 操作,Qdrant 返回最相似的 3 条记录。结果按相似度得分降序排列。
## 运行结果
运行上述代码,输出结果如下:
“`
已存储文档: 如何学习 Go 语言的并发编程
已存储文档: Python 的 asyncio 异步编程教程
已存储文档: Rust 语言所有权系统详解
已存储文档: JavaScript 前端开发最佳实践
���存���文档: Kubernetes 容器编排平台搭建
搜索Query: Go 语言并发处理
搜索结果:
– ID: 1, 内容: 如何学习 Go 语言的并发编程, 得分: 0.8642
– ID: 2, 内容: Python 的 asyncio 异步编程教程, 得分: 0.7231
– ID: 3, 内容: Rust 语言所有权系统详解, 得分: 0.6518
“`
可以看到,虽然搜索 Query 中没有出现”并发”这个词,但”Go 语言并发处理”仍然成功检索到了”如何学习 Go 语言的并发编程”这条记录,得分高达 0.8642。Python 异步编程教程得分排第二,因为 asyncio 也涉及并发概念。Rust 相关内容得分第三,虽然 Rust 的所有权系统与并发不同,但在某些上下文中可能被视为相关。
这个结果表明向量检索确实能够理解语义层面的相似性,而不仅仅是关键词匹配。对于构建问答系统、RAG 应用等场景,这种能力非常有价值。
## 总结
本文详细介绍了使用 Go 语言结合 Qdrant 构建本地向量检索系统的完整流程。从 Qdrant 的安装部署、Go 客户端的使用,到文本向量化处理和语义检索功能,提供了可运行的完整示例。
通过这个实践,可以得出以下结论:Go 语言完全可以胜任向量检索的开发需求,客户端 API 设计合理,开发体验良好。Qdrant 本地模式部署简单,不需要额外的云服务资源,适合个人开发者学习和小型项目使用。
向量检索的核心价值在于语义理解能力,即使 Query 和文档没有共同的词语,只要语义相近就能被检索到。这为构建智能问答系统、内容推荐系统、RAG 应用等提供了坚实的技术基础。
如果你的项目需要本地化的向量检索能力,建议从这个方案开始尝试。Qdrant 的官方文档提供了更详细的功能说明,包括集合管理、过滤器、分页等高级功能,可以根据实际需求进一步探索。