Qdrant 向量数据库 Golang 实战指南:从零构建高性能检索应用

本文详解 Qdrant 向量数据库在 Golang 项目中的实战应用,涵盖环境部署、SDK 接入、集合管理、向量增删改查、混合检索及过滤查询,附带报错处理与性能优化技巧,助你快速落地 AI 搜索与推荐系统。

为什么选择 Qdrant

在 AI 原生应用爆发的今天,向量数据库已成为大模型 RAG(检索增强生成)、推荐系统、图像检索等场景的核心基础设施。在众多向量数据库中,Qdrant (read: quadrant) 凭借以下优势脱颖而出:

  • 高性能:使用 Rust 编写,内存利用率极高,在过滤和检索性能上表现优异。
  • 易用性:提供了直观的 REST API 和 gRPC 接口,且支持丰富的过滤条件。
  • 功能强大:支持混合检索(向量+关键词过滤)、动态 Payload 管理、分片与复制。
  • 云原生 :支持 Kubernetes 部署,资源控制灵活。 本文将抛开枯燥的理论,直接通过 Golang 代码,带你从零实现一个基于 Qdrant 的向量检索系统。

环境准备

在开始编码前,我们需要准备好 Qdrant 服务和 Go 开发环境。

部署 Qdrant 服务

最快捷的方式是使用 Docker 启动一个单机实例:

创建 docker-compose.yaml 配置文件:

yaml 复制代码
services:
  qdrant:
    image: qdrant/qdrant:latest
    restart: always
    container_name: qdrant
    ports:
      - 6333:6333
      - 6334:6334
    environment:
      QDRANT__SERVICE__API_KEY: "password"
    expose:
      - 6333
      - 6334
      - 6335
    configs:
      - source: qdrant_config
        target: /qdrant/config/production.yaml
    volumes:
      - ./data:/qdrant/storage

configs:
  qdrant_config:
    content: |
      log_level: INFO

使用 docker compose up 启动服务

  • 6333 端口:HTTP API 接口(用于 Web UI 或 HTTP 调用)。
  • 6334 端口 :gRPC API 接口(Golang SDK 主要使用此端口,性能更高)。 启动后,访问 http://localhost:6333/dashboard 即可看到 Web 管理界面。

安装 Golang SDK

Qdrant 官方提供了维护良好的 Go 客户端。在项目中初始化并安装 SDK:

bash 复制代码
go mod init qdrant-demo
go get github.com/qdrant/go-client/qdrant

Golang 客户端连接与初始化

首先,我们需要建立与 Qdrant 的连接。Qdrant Go SDK 基于 gRPC,因此连接配置非常简单。 代码示例:初始化客户端

go 复制代码
package main

import (
	"fmt"
	"log"

	"github.com/qdrant/go-client/qdrant"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	// 1. 创建 gRPC 连接
	conn, err := grpc.Dial("localhost:6334", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
	if err != nil {
		log.Fatalf("failed to connect to Qdrant: %v", err)
	}
	defer conn.Close()
	// 2. 初始化 Qdrant Client
	qdrant.NewCollectionsClient(conn)

	// 后续代码将在这里展开...
	fmt.Println("Qdrant client connected successfully!")
}

注意 :在生产环境中,建议将 grpc.WithBlock() 去掉以实现异步连接,并添加重试机制。

核心功能实战

我们将实现一个简单的"文章语义搜索"场景。假设我们存储文章的向量(由 Embedding 模型生成)以及文章的元数据(如分类、标题、URL)。

创建集合

在 Qdrant 中,数据存储在 Collection(集合)中。创建集合时必须指定向量维度。 代码示例:创建集合 articles

go 复制代码
func createCollection(ctx context.Context, client qdrant.CollectionsClient) error {
	req := &qdrant.CreateCollection{
		CollectionName: "articles",
		// 768 维向量模型 (如 BAAI/bge-base-zh),使用余弦相似度
		VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
			Size:     768,
			Distance: qdrant.Distance_Cosine,
			HnswConfig: &qdrant.HnswConfigDiff{
				M:           qdrant.PtrOf(uint64(16)),  // HNSW 图的连接度,影响召回率与速度
				EfConstruct: qdrant.PtrOf(uint64(100)), // 构建索引时的搜索范围
			},
		}),
		OptimizersConfig: &qdrant.OptimizersConfigDiff{
			IndexingThreshold: qdrant.PtrOf(uint64(10000)), // 数据量达到该值时开始索引
		},
	}
	_, err := client.Create(ctx, req)
	if err != nil {
		// 集合已存在时直接跳过,保证脚本可重复执行
		if strings.Contains(err.Error(), "already exists") {
			fmt.Println("Collection 'articles' already exists, skip creation.")
			return nil
		}
		return fmt.Errorf("failed to create collection: %w", err)
	}
	fmt.Println("Collection 'articles' created successfully.")
	return nil
}

向量数据插入

Qdrant 不仅仅是存向量,还能存 Payload(结构化数据)。这是 Qdrant 相比纯向量库的一大优势,允许我们在搜索时进行精细过滤。 代码示例:插入数据

go 复制代码
func upsertPoints(ctx context.Context, client qdrant.PointsClient) error {
	points := []*qdrant.PointStruct{
		{
			Id:      &qdrant.PointId{PointIdOptions: &qdrant.PointId_Num{Num: 1}},
			Vectors: qdrant.NewVectorsDense(generateFakeVector(768)), // 模拟向量
			Payload: map[string]*qdrant.Value{
				"title":    {Kind: &qdrant.Value_StringValue{StringValue: "Go语言最佳实践"}},
				"category": {Kind: &qdrant.Value_StringValue{StringValue: "tech"}},
				"views":    {Kind: &qdrant.Value_IntegerValue{IntegerValue: 1000}},
			},
		},
		{
			Id:      &qdrant.PointId{PointIdOptions: &qdrant.PointId_Num{Num: 2}},
			Vectors: qdrant.NewVectorsDense(generateFakeVector(768)), // 模拟向量
			Payload: map[string]*qdrant.Value{
				"title":    {Kind: &qdrant.Value_StringValue{StringValue: "深度学习入门指南"}},
				"category": {Kind: &qdrant.Value_StringValue{StringValue: "ai"}},
				"views":    {Kind: &qdrant.Value_IntegerValue{IntegerValue: 5000}},
			},
		},
	}
	req := &qdrant.UpsertPoints{
		CollectionName: "articles",
		Points:         points,
	}
	_, err := client.Upsert(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to upsert points: %w", err)
	}
	fmt.Println("Points upserted successfully.")
	return nil
}

// 辅助函数:生成模拟向量
func generateFakeVector(size int) []float32 {
	vec := make([]float32, size)
	for i := range vec {
		vec[i] = 0.1 // 仅作演示,实际应使用 Embedding 模型
	}
	return vec
}

相似度检索

这是最核心的功能:给定向量,返回最相似的 K 个结果。 代码示例:基础搜索

go 复制代码
func searchPoints(ctx context.Context, client qdrant.PointsClient) error {
	queryVector := generateFakeVector(768) // 模拟查询向量
	req := &qdrant.SearchPoints{
		CollectionName: "articles",
		Vector:         queryVector,
		Limit:          uint64(3),                                                                                      // 返回 Top 3
		WithPayload:    &qdrant.WithPayloadSelector{SelectorOptions: &qdrant.WithPayloadSelector_Enable{Enable: true}}, // 返回 Payload
	}
	resp, err := client.Search(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to search points: %w", err)
	}
	fmt.Println("Search Results:")
	for _, result := range resp.Result {
		fmt.Printf("ID: %d, Score: %.4f, Title: %s\n",
			result.Id.GetNum(),
			result.Score,
			result.Payload["title"].GetStringValue())
	}
	return nil
}

条件过滤检索

实战中,我们经常需要"既相似,又满足特定条件"的数据。例如:"查找关于'Go语言'的文章,且阅读量大于 500"。这就是 Qdrant 的 Filter 功能。 代码示例:带过滤条件的搜索

go 复制代码
func searchWithFilter(ctx context.Context, client qdrant.PointsClient) error {
	queryVector := generateFakeVector(768)
	// 构建过滤条件:Category 必须是 'tech' 且 Views 大于 500
	req := &qdrant.SearchPoints{
		CollectionName: "articles",
		Vector:         queryVector,
		Limit:          uint64(10),
		WithPayload:    &qdrant.WithPayloadSelector{SelectorOptions: &qdrant.WithPayloadSelector_Enable{Enable: true}}, // 返回 Payload
		Filter: &qdrant.Filter{
			Must: []*qdrant.Condition{
				// 条件1:Category 等于 'tech'
				{
					ConditionOneOf: &qdrant.Condition_Field{
						Field: &qdrant.FieldCondition{
							Key: "category",
							Match: &qdrant.Match{
								MatchValue: &qdrant.Match_Keyword{Keyword: "tech"},
							},
						},
					},
				},
				// 条件2:Views 大于 500
				{
					ConditionOneOf: &qdrant.Condition_Field{
						Field: &qdrant.FieldCondition{
							Key: "views",
							Range: &qdrant.Range{
								Gt: qdrant.PtrOf(float64(500)),
							},
						},
					},
				},
			},
		},
	}
	resp, err := client.Search(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to search with filter: %w", err)
	}
	fmt.Println("Filtered Search Results (Category=tech, Views>500):")
	for _, result := range resp.Result {
		fmt.Printf("ID: %d, Score: %.4f, Title: %s, Views: %d\n",
			result.Id.GetNum(),
			result.Score,
			result.Payload["title"].GetStringValue(),
			result.Payload["views"].GetIntegerValue())
	}
	return nil
}

数据更新与删除

业务数据是动态变化的,Qdrant 支持对 Payload 和向量进行更新,以及删除特定 ID 的数据。 代码示例:更新与删除

go 复制代码
func updateAndDelete(ctx context.Context, client qdrant.PointsClient) error {
	// 1. 更新 Payload (将 ID 为 1 的文章阅读量改为 9999)
	setPayloadReq := &qdrant.SetPayloadPoints{
		CollectionName: "articles",
		Payload: map[string]*qdrant.Value{
			"views": {Kind: &qdrant.Value_IntegerValue{IntegerValue: 9999}},
		},
		PointsSelector: &qdrant.PointsSelector{
			PointsSelectorOneOf: &qdrant.PointsSelector_Points{Points: &qdrant.PointsIdsList{
				Ids: []*qdrant.PointId{{PointIdOptions: &qdrant.PointId_Num{Num: 1}}},
			}},
		},
	}
	_, err := client.SetPayload(ctx, setPayloadReq)
	if err != nil {
		return fmt.Errorf("failed to update payload: %w", err)
	}
	fmt.Println("Payload updated for ID 1.")
	// 2. 删除数据 (删除 ID 为 2 的数据)
	deleteReq := &qdrant.DeletePoints{
		CollectionName: "articles",
		Points: &qdrant.PointsSelector{
			PointsSelectorOneOf: &qdrant.PointsSelector_Points{Points: &qdrant.PointsIdsList{
				Ids: []*qdrant.PointId{{PointIdOptions: &qdrant.PointId_Num{Num: 2}}},
			}},
		},
	}
	_, err = client.Delete(ctx, deleteReq)
	if err != nil {
		return fmt.Errorf("failed to delete point: %w", err)
	}
	fmt.Println("Point ID 2 deleted.")
	return nil
}

性能优化技巧

为了让 Qdrant 在生产环境跑得更快,请注意以下几点:

批量插入

不要逐条调用 Upsert。Qdrant 对批量写入做了深度优化。建议将数据积攒到 100-1000 条后批量插入,可以显著提高吞吐量。

调整 HNSW 参数

HNSW 是 Qdrant 的核心索引算法:

  • m:最大连接数。默认 16。增大可提高召回率,但消耗更多内存。
  • ef_construct:构建索引时的搜索范围。默认 100。增大可提高索引质量,但构建变慢。
  • 搜索时的 ef :可以在搜索请求中动态指定。增大 ef 可以提高召回率,但会降低搜索速度。对于实时性要求高的接口,设置小一点(如 50);对于离线分析,设置大一点(如 200)。

Payload 索引

如果你经常按"城市"、"类别"等字段过滤,务必为这些字段建立索引。

go 复制代码
createCollection 的 payload_index_config 参数示例:
PayloadIndexConfig: []*qdrant.PayloadIndexParams{
    {
        FieldName: "category",
        FieldType: qdrant.PayloadIndexType_Keyword,
    },
}

量化

如果你的内存吃紧,可以在创建集合时开启量化(Scalar Quantization 或 Product Quantization)。这会将 float32 的向量压缩为 int8uint8,虽然会损失极少量的精度,但能节省 4 倍的内存。

总结

本文通过详尽的代码示例,展示了如何在 Golang 中集成 Qdrant 向量数据库,涵盖了从环境搭建到核心 CRUD、过滤检索的全过程。Qdrant 结合 Golang 的高并发特性,非常适合构建高性能的 AI 应用后端。 掌握了这些基础知识后,你可以进一步探索 Qdrant 的高级特性,如 多向量存储 (同一份数据存不同模型的向量)、负向量搜索 (推荐系统常用)以及 分布式集群部署,为你的业务赋能。

相关推荐
hsfxuebao2 小时前
OpenSpec 完整使用流程笔记 (SDD)
后端
唐青枫2 小时前
别把 struct 只当成数据盒子:Zig 结构体从入门到实战
后端
why技术2 小时前
AI 写的文章,可能都带着手敲一遍都去不掉的“隐形水印”。
前端·人工智能·后端
北斗落凡尘2 小时前
LangGraph 入门实战(9)--中断
后端·langchain
CodeSheep2 小时前
又一个华为天才少年,离职了!
前端·后端·程序员
IT_陈寒4 小时前
Vue的双向绑定把我坑惨了,原来这个场景不能用
前端·人工智能·后端
SomeB1oody5 小时前
【RustyML入门】4.2. 标准化与归一化
开发语言·后端·机器学习·rust·教程
31535669135 小时前
DeepSeek Harness 发布后,我没急着跑 Demo,先把 `.agents/` 翻了一遍
前端·后端·github
Nturmoils5 小时前
不在公司,也能连回办公电脑:用 Natapp 打通 Windows 远程桌面
后端