Milvus 向量数据库完整教程
Go 版本 + Ollama bge-m3 向量模型
目录
- 系统架构
- 核心组件介绍
- [Milvus 向量数据库](#Milvus 向量数据库)
- [Ollama 本地推理引擎](#Ollama 本地推理引擎)
- [BGE-M3 向量模型](#BGE-M3 向量模型)
- [Attu 可视化管理界面](#Attu 可视化管理界面)
- 进阶:重排序技术
- 环境准备
- 快速开始
- 教程代码详解
- [Go SDK API 参考](#Go SDK API 参考)
- 常用操作示例
- 进阶主题
- 文章向量化策略
- [Milvus 高可用架构](#Milvus 高可用架构)
- 参考资源
git地址:https://gitee.com/os-lee/ai 查看milvus-demo
系统架构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 整体系统架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ 用户查询 │ │
│ │ "学Python" │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Ollama 本地推理引擎 │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ BGE-M3 向量模型 │ │ │
│ │ │ (Embedding Model) │ │ │
│ │ │ 文本 → 1024维向量 │ │ │
│ │ └──────────┬──────────────────────────────────────────────────────┘ │ │
│ └─────────────┼───────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ Milvus 向量数据库 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Collection: book_embeddings │ │ │
│ │ ├──────────────────────────────┤ │ │
│ │ │ book_id │ INT64 (PK) │ │ │
│ │ │ book_name │ VARCHAR(256) │ │ │
│ │ │ author │ VARCHAR(128) │ │ │
│ │ │ category │ VARCHAR(64) │ │ │
│ │ │ book_intro│ VARCHAR(1024) │ │ │
│ │ │ embedding │ VECTOR(1024) │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ ANN 搜索 (Top K) │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ IVF_FLAT 索引 (L2 距离) │ │ │
│ │ └──────────────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 搜索结果 (按距离排序) │ │
│ │ 距离越小 = 语义越相似 │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Attu 可视化管理界面 │ │
│ │ http://localhost:18000 │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
核心组件介绍
1. Milvus 向量数据库
Milvus 是一个开源的向量数据库,专为嵌入向量(Embedding Vector)相似性搜索设计。
核心能力
| 能力 | 说明 |
|---|---|
| 📥 海量存储 | 支持数十亿级向量数据存储 |
| 🔍 毫秒搜索 | ANN(近似最近邻)算法实现毫秒级检索 |
| 📊 混合查询 | 向量搜索 + 标量过滤组合使用 |
| 📈 多种索引 | FLAT、IVF_FLAT、HNSW 等多种索引类型 |
| 🔄 分布式 | 支持水平扩展,应对大规模数据 |
应用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 🔍 语义搜索 | 理解查询意图而非关键词匹配 | "想学编程" → 推荐 Python 教程 |
| 🖼️ 图像搜索 | 以图搜图 | 上传商品图 → 找相似商品 |
| 🤖 RAG 应用 | 为 LLM 提供知识增强 | ChatGPT + 企业知识库 |
| 👤 推荐系统 | 基于相似度的推荐 | 喜欢 A 书 → 推荐相似书籍 |
| 🎵 音视频检索 | 内容特征匹配 | 哼唱旋律 → 找到歌曲 |
Milvus 核心概念
go
// ==================== Collection(集合)====================
// 类似关系数据库的"表",是数据存储的容器
//
// 概念对比:
// Collection = Table(表)
// Schema = CREATE TABLE(表结构)
// Entity = Row(一行数据)
// ==================== 字段类型 ====================
// 主键字段(必须有一个)
entity.FieldTypeInt64 // 整型主键,支持自增
entity.FieldTypeVarChar // 字符串主键
// 标量字段(用于过滤条件)
entity.FieldTypeVarChar // 字符串,需指定 max_length
entity.FieldTypeInt64 // 整型
entity.FieldTypeBool // 布尔
entity.FieldTypeFloat // 浮点数
entity.FieldTypeJSON // JSON 类型
// 向量字段(必须有一个,用于相似度搜索)
entity.FieldTypeFloatVector // 浮点向量(最常用)
entity.FieldTypeBinaryVector // 二进制向量(节省空间)
索引类型详解
| 索引类型 | 特点 | 适用场景 | 参数 |
|---|---|---|---|
| FLAT | 暴力搜索,100% 精确 | 小数据量(< 10万) | 无 |
| IVF_FLAT | 倒排索引,平衡速度与精度 | 中等数据量(本教程使用) | nlist(聚类数) |
| IVF_SQ8 | 量化压缩,节省内存 | 内存受限场景 | nlist |
| HNSW | 图索引,极速搜索 | 追求低延迟 | M, efConstruction |
距离度量(Metric Type)
| 类型 | Go 常量 | 说明 | 适用场景 |
|---|---|---|---|
| L2 | entity.L2 |
欧氏距离,越小越相似 | 通用场景(本教程使用) |
| IP | entity.IP |
内积,越大越相似 | 归一化向量 |
| COSINE | entity.COSINE |
余弦相似度,越大越相似 | 文本相似度 |
2. Ollama 本地推理引擎
Ollama 是一个轻量级的本地大模型运行框架,支持运行各种开源模型。
核心特点
| 特性 | 说明 |
|---|---|
| 🏠 本地运行 | 无需联网,数据不出本地 |
| 🚀 简单部署 | 一行命令拉取模型 |
| 💰 完全免费 | 无 API 调用费用 |
| 📦 模型丰富 | 支持 LLM、Embedding、Reranker 等 |
| 🔧 CPU 友好 | 无需 GPU 也能运行 |
API 接口
bash
# Embedding API - 获取文本向量
POST http://localhost:11434/api/embeddings
{
"model": "bge-m3",
"prompt": "要转换的文本"
}
# 响应: { "embedding": [0.123, -0.456, ...] } # 1024 维向量
# 模型管理
ollama list # 列出已安装模型
ollama pull <model> # 拉取模型
ollama rm <model> # 删除模型
ollama serve # 启动服务
3. BGE-M3 向量模型
BGE-M3(BAAI General Embedding - Multi-Lingual, Multi-Functionality, Multi-Granularity)是由北京智源人工智能研究院(BAAI)开发的开源向量模型。
模型特点
| 特性 | 说明 |
|---|---|
| 🌍 多语言 | 支持 100+ 语言,中英文效果优异 |
| 📐 向量维度 | 1024 维(本教程使用) |
| 🎯 多功能 | 同时支持稠密向量、稀疏向量、多向量检索 |
| 📏 长文本 | 支持最长 8192 tokens |
| 🏆 效果领先 | MTEB 排行榜多项第一 |
工作原理
┌─────────────────────────────────────────────────────────────────────────┐
│ BGE-M3 向量化过程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 输入文本 模型处理 输出向量 │
│ │
│ "一本面向初学者的 ──────► Transformer ──────► [0.12, 0.45, │
│ Python教程" 编码器 -0.23, ...] │
│ (1024 维) │
│ │
│ 语义相似的文本会得到相近的向量: │
│ │
│ "Python入门教程" ───────────────► 向量 A │
│ "学习Python编程" ───────────────► 向量 B ← 距离很近! │
│ "深度学习神经网络" ───────────────► 向量 C ← 距离较远 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Go 代码实现
go
// OllamaEmbeddingRequest Ollama embedding 请求结构
type OllamaEmbeddingRequest struct {
Model string `json:"model"` // 模型名称: "bge-m3"
Prompt string `json:"prompt"` // 要转换的文本
}
// OllamaEmbeddingResponse Ollama embedding 响应结构
type OllamaEmbeddingResponse struct {
Embedding []float64 `json:"embedding"` // 1024 维向量
}
// getEmbedding 调用 Ollama API 获取文本的向量表示
func getEmbedding(text string) ([]float32, error) {
reqBody := OllamaEmbeddingRequest{
Model: "bge-m3",
Prompt: text,
}
jsonData, _ := json.Marshal(reqBody)
resp, err := http.Post(
"http://localhost:11434/api/embeddings",
"application/json",
bytes.NewBuffer(jsonData),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result OllamaEmbeddingResponse
json.NewDecoder(resp.Body).Decode(&result)
// float64 → float32(Milvus 使用 float32)
embedding := make([]float32, len(result.Embedding))
for i, v := range result.Embedding {
embedding[i] = float32(v)
}
return embedding, nil
}
与其他模型对比
| 模型 | 维度 | 中文效果 | 英文效果 | 模型大小 | 特点 |
|---|---|---|---|---|---|
| BGE-M3 ✅ | 1024 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 2.2GB | 多语言、效果最佳 |
| bge-large-zh | 1024 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 1.3GB | 中文专优 |
| nomic-embed-text | 768 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 550MB | 轻量级 |
| all-MiniLM-L6-v2 | 384 | ⭐⭐ | ⭐⭐⭐⭐ | 80MB | 超轻量 |
4. Attu 可视化管理界面
Attu 是 Milvus 官方的图形化管理工具,提供直观的 Web 界面。
核心功能
| 功能 | 说明 |
|---|---|
| 📊 Collection 管理 | 创建、删除、查看 Collection |
| 🔍 数据浏览 | 查看和搜索存储的数据 |
| 📈 索引管理 | 创建、删除索引 |
| 📝 Schema 查看 | 查看字段结构 |
| 🔧 性能监控 | 查看系统状态 |
访问地址
- URL: http://localhost:18000
- Milvus 地址: standalone:19530(Docker 内部网络)
环境准备
1. 部署 Ollama 和模型
bash
# 1. 安装 Ollama
# Linux/Mac
curl -fsSL https://ollama.com/install.sh | sh
# Windows: 下载安装包 https://ollama.com/download
# 2. 启动 Ollama 服务
ollama serve
# 3. 拉取向量模型(约 2.2GB)
ollama pull bge-m3
# 4. 验证模型
ollama list
# 输出:
# NAME SIZE
# bge-m3:latest 2.2 GB
2. 部署 Milvus(Docker Compose)
创建 docker-compose.yaml:
yaml
version: '3.5'
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.5
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- etcd_data:/etcd
command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
volumes:
- minio_data:/minio_data
command: minio server /minio_data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v2.4.0
command: ["milvus", "run", "standalone"]
environment:
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- milvus_data:/var/lib/milvus
ports:
- "19530:19530" # Milvus gRPC 端口
- "9091:9091" # 健康检查端口
depends_on:
- etcd
- minio
attu:
container_name: milvus-attu
image: zilliz/attu:v2.4
environment:
MILVUS_URL: standalone:19530
ports:
- "18000:3000" # Attu Web 管理界面
depends_on:
- standalone
volumes:
etcd_data:
minio_data:
milvus_data:
启动服务:
bash
docker-compose up -d
3. 验证部署
| 服务 | 地址 | 说明 |
|---|---|---|
| Milvus | localhost:19530 | gRPC 接口 |
| Attu | http://localhost:18000 | Web 管理界面 |
| Ollama | http://localhost:11434 | API 接口 |
4. 安装 Go SDK
bash
go get github.com/milvus-io/milvus-sdk-go/v2
快速开始
示例文件说明
本教程包含两个示例程序:
| 示例文件 | 说明 | 适用场景 |
|---|---|---|
tutorial.go |
基础教程,演示 Milvus 完整操作流程 | 入门学习、书籍数据 |
article/article.go |
文章向量库示例,支持短篇整体向量化和长篇分块向量化 | 知识库、文章推荐 |
运行基础教程
bash
cd test/milvus-demo
# 如果还没有 go.mod
go mod init milvus-demo
# 安装依赖
go get github.com/milvus-io/milvus-sdk-go/v2
# 运行基础教程(书籍向量库)
go run tutorial.go
运行文章向量库示例
bash
cd test/milvus-demo/article
# 运行文章示例(短篇 + 长篇分块)
go run article.go
配置说明
tutorial.go 配置:
go
const (
// Milvus 连接配置
MilvusHost = "localhost"
MilvusPort = "19530"
CollectionName = "book_embeddings_go"
VectorDim = 1024 // bge-m3 输出 1024 维向量
// Ollama 配置
OllamaHost = "http://localhost:11434"
// 向量模型:bge-m3
// 多语言、多功能的向量模型,输出 1024 维向量
EmbeddingModel = "bge-m3"
)
article/article.go 配置:
go
const (
// Milvus 连接配置
ArticleMilvusHost = "localhost"
ArticleMilvusPort = "19530"
ArticleCollectionName = "article_embeddings" // 短篇文章集合
ChunkedCollectionName = "article_chunks_embeddings" // 长篇文章分块集合
ArticleVectorDim = 1024
// Ollama 配置
ArticleOllamaHost = "http://localhost:11434"
ArticleEmbedModel = "bge-m3"
// 分块配置
ChunkSize = 500 // 每块约 500 字
ChunkOverlap = 50 // 重叠 50 字
)
教程代码详解
一、tutorial.go - 基础教程
tutorial.go 包含 10 个核心步骤:
步骤概览
| 步骤 | 功能 | 核心方法 | 说明 |
|---|---|---|---|
| 1 | 连接 Milvus | client.NewClient() |
建立 gRPC 连接 |
| 2 | 创建 Collection | c.CreateCollection() |
定义数据结构 |
| 3 | 插入数据 | c.Insert() + c.Flush() |
使用 BGE-M3 生成向量 |
| 4 | 创建索引 | c.CreateIndex() |
IVF_FLAT 索引 |
| 5 | 加载到内存 | c.LoadCollection() |
搜索前必须执行 |
| 6 | 向量搜索 | c.Search() |
语义相似度搜索 |
| 7 | 带过滤搜索 | c.Search() + expr |
标量过滤 |
| 8 | 标量查询 | c.Query() |
不涉及向量 |
| 9 | 删除数据 | c.Delete() |
按条件删除 |
| 10 | 释放资源 | c.ReleaseCollection() |
释放内存 |
详细说明
步骤 1:连接 Milvus
go
// step1Connect 建立与 Milvus 的 gRPC 连接
func step1Connect(ctx context.Context) client.Client {
milvusClient, err := client.NewClient(ctx, client.Config{
Address: fmt.Sprintf("%s:%s", MilvusHost, MilvusPort),
// 如果启用了认证:
// Username: "root",
// Password: "Milvus",
})
if err != nil {
log.Fatalf("连接 Milvus 失败: %v", err)
}
return milvusClient
}
步骤 2:创建 Collection
go
// step2CreateCollection 创建 Collection 并定义 Schema
func step2CreateCollection(ctx context.Context, c client.Client) {
schema := &entity.Schema{
CollectionName: CollectionName,
Description: "书籍向量数据库 - Go 版本",
AutoID: true, // 主键自动生成
Fields: []*entity.Field{
// 主键字段
{
Name: "book_id",
DataType: entity.FieldTypeInt64,
PrimaryKey: true,
AutoID: true,
},
// 标量字段
{
Name: "book_name",
DataType: entity.FieldTypeVarChar,
TypeParams: map[string]string{"max_length": "256"},
},
{
Name: "author",
DataType: entity.FieldTypeVarChar,
TypeParams: map[string]string{"max_length": "128"},
},
{
Name: "category",
DataType: entity.FieldTypeVarChar,
TypeParams: map[string]string{"max_length": "64"},
},
{
Name: "book_intro",
DataType: entity.FieldTypeVarChar,
TypeParams: map[string]string{"max_length": "1024"},
},
// 向量字段
{
Name: "embedding",
DataType: entity.FieldTypeFloatVector,
TypeParams: map[string]string{"dim": "1024"}, // BGE-M3 维度
},
},
}
err := c.CreateCollection(ctx, schema, entity.DefaultShardNumber)
}
字段结构:
| 字段名 | 类型 | 说明 |
|---|---|---|
| book_id | INT64 (PK) | 主键,自动生成 |
| book_name | VARCHAR(256) | 书名 |
| author | VARCHAR(128) | 作者 |
| category | VARCHAR(64) | 分类(用于过滤) |
| book_intro | VARCHAR(1024) | 简介(用于生成向量) |
| embedding | VECTOR(1024) | BGE-M3 向量 |
步骤 3:插入数据
go
// step3InsertData 使用 BGE-M3 生成向量并插入数据
func step3InsertData(ctx context.Context, c client.Client) {
// 准备数据
bookNames := []string{"Python编程", "深度学习", ...}
authors := []string{"Eric Matthes", "Ian Goodfellow", ...}
categories := []string{"编程", "AI", ...}
intros := []string{"一本面向初学者的Python教程", ...}
// 使用 BGE-M3 生成向量
// 关键:将书籍简介转换为 1024 维向量
embeddings, err := getEmbeddings(intros)
// 构建列数据
bookNameColumn := entity.NewColumnVarChar("book_name", bookNames)
authorColumn := entity.NewColumnVarChar("author", authors)
categoryColumn := entity.NewColumnVarChar("category", categories)
introColumn := entity.NewColumnVarChar("book_intro", intros)
embeddingColumn := entity.NewColumnFloatVector("embedding", VectorDim, embeddings)
// 执行插入
_, err = c.Insert(ctx, CollectionName, "",
bookNameColumn, authorColumn, categoryColumn, introColumn, embeddingColumn)
// 刷新到磁盘(确保持久化)
c.Flush(ctx, CollectionName, false)
}
步骤 4:创建索引
go
// step4CreateIndex 创建 IVF_FLAT 索引
func step4CreateIndex(ctx context.Context, c client.Client) {
// IVF_FLAT 索引参数
// - L2: 欧氏距离(越小越相似)
// - 128: nlist 聚类中心数量
idx, err := entity.NewIndexIvfFlat(entity.L2, 128)
// 在 embedding 字段上创建索引
err = c.CreateIndex(ctx, CollectionName, "embedding", idx, false)
}
索引参数说明:
| 参数 | 值 | 说明 |
|---|---|---|
| 索引类型 | IVF_FLAT | 倒排索引,平衡速度与精度 |
| 距离度量 | L2 | 欧氏距离,越小越相似 |
| nlist | 128 | 聚类中心数量 |
步骤 5:加载到内存
go
// step5LoadCollection 加载 Collection 到内存
func step5LoadCollection(ctx context.Context, c client.Client) {
// 必须加载后才能搜索!
err := c.LoadCollection(ctx, CollectionName, false)
}
⚠️ 注意 :搜索前必须执行
LoadCollection,否则会报错!
步骤 6:向量搜索
go
// step6VectorSearch 执行向量相似度搜索
func step6VectorSearch(ctx context.Context, c client.Client) {
queryText := "想学习 Python 编程入门"
// 1. 使用 BGE-M3 将查询转换为向量
queryEmb, _ := getEmbedding(queryText)
queryVector := []entity.Vector{entity.FloatVector(queryEmb)}
// 2. 搜索参数
sp, _ := entity.NewIndexIvfFlatSearchParam(16) // nprobe = 16
// 3. 执行向量搜索
results, _ := c.Search(
ctx,
CollectionName,
nil, // partitions
"", // filter expression
[]string{"book_name", "author", "category"}, // output fields
queryVector,
"embedding",
entity.L2,
5, // topK
sp,
)
// 4. 输出搜索结果
for _, result := range results {
for i := 0; i < result.ResultCount; i++ {
bookName, _ := result.Fields.GetColumn("book_name").GetAsString(i)
distance := result.Scores[i]
fmt.Printf("书名: %s, 距离: %.4f\n", bookName, distance)
}
}
}
向量搜索流程图:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户查询 │ ──→ │ BGE-M3 向量化│ ──→ │ Milvus 搜索 │ ──→ Top K 结果
│ "学Python" │ │ (1024 维) │ │ (ANN 算法) │
└─────────────┘ └─────────────┘ └─────────────┘
步骤 7:带过滤条件的搜索
go
// step7SearchWithFilter 带标量过滤的向量搜索
func step7SearchWithFilter(ctx context.Context, c client.Client) {
queryText := "深度学习神经网络"
queryEmb, _ := getEmbedding(queryText)
queryVector := []entity.Vector{entity.FloatVector(queryEmb)}
sp, _ := entity.NewIndexIvfFlatSearchParam(16)
// 过滤表达式:只在 AI 类书籍中搜索
filterExpr := `category == "AI"`
results, _ := c.Search(
ctx,
CollectionName,
nil,
filterExpr, // 关键:添加过滤表达式
[]string{"book_name", "author", "category"},
queryVector,
"embedding",
entity.L2,
5,
sp,
)
}
常用过滤表达式:
go
// 精确匹配
`category == "AI"`
// 多值匹配
`category in ["AI", "编程", "数据库"]`
// 数值比较
`book_id > 100`
`price >= 50 && price <= 100`
// 字符串前缀匹配
`book_name like "Python%"`
// 组合条件
`category == "AI" && price < 100`
`(category == "AI" || category == "编程") && in_stock == true`
步骤 8:标量查询(Query)
go
// step8QueryData 标量查询(不涉及向量)
func step8QueryData(ctx context.Context, c client.Client) {
// Query 类似 SQL SELECT ... WHERE
results, _ := c.Query(
ctx,
CollectionName,
nil, // partitions
`category == "数据库"`, // 过滤表达式
[]string{"book_id", "book_name", "author"}, // 返回字段
)
// 解析结果
idCol := results.GetColumn("book_id")
nameCol := results.GetColumn("book_name")
for i := 0; i < idCol.Len(); i++ {
id, _ := idCol.GetAsInt64(i)
name, _ := nameCol.GetAsString(i)
fmt.Printf("ID: %d, 书名: %s\n", id, name)
}
}
步骤 9:删除数据
go
// step9DeleteData 按条件删除数据
func step9DeleteData(ctx context.Context, c client.Client) {
// 删除所有运维类书籍
err := c.Delete(ctx, CollectionName, "", `category == "运维"`)
}
步骤 10:释放资源
go
// step10Cleanup 释放资源
func step10Cleanup(ctx context.Context, c client.Client) {
// 从内存释放(数据仍在磁盘)
c.ReleaseCollection(ctx, CollectionName)
// 可选:删除整个 Collection
// c.DropCollection(ctx, CollectionName)
// 关闭连接
c.Close()
}
二、article/article.go - 文章向量库示例
article/article.go 演示了两种文章向量化场景:
| 场景 | 说明 | 向量化方式 |
|---|---|---|
| 短篇文章 | ≤ 2000 字的文章 | 整体向量化:标题 + 摘要 + 内容 → 1 个向量 |
| 长篇文章 | > 2000 字的文章 | 分块向量化:按 500 字分块 + 50 字重叠 → N 个向量 |
功能概览
┌─────────────────────────────────────────────────────────────────────────────┐
│ article.go 功能架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 短篇文章向量库 (article_embeddings) │ │
│ │ │ │
│ │ • 12 篇示例文章(技术、生活、科学、历史、文化) │ │
│ │ • 整体向量化:标题 + 摘要 + 内容 → 1024 维向量 │ │
│ │ • 支持语义搜索、分类过滤搜索 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 长篇文章分块向量库 (article_chunks_embeddings) │ │
│ │ │ │
│ │ • 1 篇 2000+ 字长文章(Go 语言完整教程) │ │
│ │ • 分块向量化:500 字/块 + 50 字重叠 │ │
│ │ • 每块带原文章 UUID,支持结果溯源 │ │
│ │ • 支持块级语义搜索 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
短篇文章 Collection Schema
go
schema := &entity.Schema{
CollectionName: "article_embeddings",
Fields: []*entity.Field{
{Name: "article_id", DataType: entity.FieldTypeInt64, PrimaryKey: true, AutoID: true},
{Name: "title", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "256"}},
{Name: "author", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "64"}},
{Name: "category", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "32"}},
{Name: "tags", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "256"}},
{Name: "summary", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "512"}},
{Name: "content", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "4096"}},
{Name: "create_time", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "32"}},
{Name: "publish_time", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "32"}},
{Name: "embedding", DataType: entity.FieldTypeFloatVector, TypeParams: map[string]string{"dim": "1024"}},
},
}
长篇文章分块 Collection Schema
go
schema := &entity.Schema{
CollectionName: "article_chunks_embeddings",
Fields: []*entity.Field{
{Name: "chunk_id", DataType: entity.FieldTypeInt64, PrimaryKey: true, AutoID: true},
{Name: "article_uuid", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "64"}},
{Name: "title", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "256"}},
{Name: "chunk_index", DataType: entity.FieldTypeInt32}, // 块索引:0, 1, 2, ...
{Name: "chunk_total", DataType: entity.FieldTypeInt32}, // 总块数
{Name: "chunk_content", DataType: entity.FieldTypeVarChar, TypeParams: map[string]string{"max_length": "2048"}},
{Name: "embedding", DataType: entity.FieldTypeFloatVector, TypeParams: map[string]string{"dim": "1024"}},
},
}
分块算法实现
go
// ChunkConfig 分块配置
type ChunkConfig struct {
ChunkSize int // 每块目标大小(字符数)
ChunkOverlap int // 重叠大小(字符数)
}
// DefaultChunkConfig 推荐配置
var DefaultChunkConfig = ChunkConfig{
ChunkSize: 500, // 每块约 500 字
ChunkOverlap: 50, // 重叠 50 字(10%)
}
// splitToChunks 将长文本拆分为多个块
func splitToChunks(content string, config ChunkConfig) []string {
runes := []rune(content)
var chunks []string
for i := 0; i < len(runes); {
end := i + config.ChunkSize
if end > len(runes) {
end = len(runes)
}
chunk := string(runes[i:end])
chunks = append(chunks, chunk)
// 下一块起始位置 = 当前位置 + 块大小 - 重叠大小
i += config.ChunkSize - config.ChunkOverlap
if end == len(runes) {
break
}
}
return chunks
}
长篇文章插入流程
go
func insertLongArticle(ctx context.Context, c client.Client, article LongArticle) {
// 1. 生成文章 UUID
articleUUID := uuid.New().String()
// 2. 将内容分块
chunks := splitToChunks(article.Content, DefaultChunkConfig)
chunkTotal := len(chunks)
// 3. 为每个块生成向量并插入
for i, chunk := range chunks {
// 每个块带上标题信息,增强语义关联
textForEmbedding := fmt.Sprintf("%s - 第%d部分: %s", article.Title, i+1, chunk)
embedding, _ := getArticleEmbedding(textForEmbedding)
// 插入到分块 Collection
// article_uuid: 用于关联同一篇文章的所有块
// chunk_index: 块索引,用于排序
// chunk_total: 总块数,用于判断完整性
}
}
分块搜索与结果合并
go
// 搜索时返回块级结果
results, _ := c.Search(ctx, "article_chunks_embeddings", ...)
// 根据 article_uuid 可以:
// 1. 获取匹配块的上下文(前后块)
// 2. 聚合同一文章的多个匹配块
// 3. 定位到原文的具体位置
Go SDK API 参考
连接管理
go
// 创建连接
c, err := client.NewClient(ctx, client.Config{
Address: "localhost:19530",
Username: "root", // 可选
Password: "Milvus", // 可选
})
// 关闭连接
c.Close()
Collection 操作
go
// 列出所有 Collection
collections, err := c.ListCollections(ctx)
// 检查是否存在
has, err := c.HasCollection(ctx, "name")
// 获取信息
collection, err := c.DescribeCollection(ctx, "name")
// 获取数据量
stats, err := c.GetCollectionStatistics(ctx, "name")
// 删除
err := c.DropCollection(ctx, "name")
数据操作
go
// 插入
result, err := c.Insert(ctx, collectionName, partitionName, columns...)
// 刷新到磁盘
err := c.Flush(ctx, collectionName, false)
// 删除
err := c.Delete(ctx, collectionName, partitionName, expr)
索引操作
go
// 创建不同类型的索引
idxFlat, _ := entity.NewIndexFlat(entity.L2)
idxIvf, _ := entity.NewIndexIvfFlat(entity.L2, 128)
idxHnsw, _ := entity.NewIndexHNSW(entity.L2, 16, 256)
// 创建索引
err := c.CreateIndex(ctx, collectionName, fieldName, index, false)
// 获取索引信息
indexes, err := c.DescribeIndex(ctx, collectionName, fieldName)
// 删除索引
err := c.DropIndex(ctx, collectionName, fieldName)
搜索参数
go
// IVF 类索引
sp, _ := entity.NewIndexIvfFlatSearchParam(nprobe)
// HNSW 索引
sp, _ := entity.NewIndexHNSWSearchParam(ef)
// FLAT 索引(无参数)
sp, _ := entity.NewIndexFlatSearchParam()
常用操作示例
批量插入大数据
go
// 分批插入,每批 10000 条
batchSize := 10000
for i := 0; i < totalCount; i += batchSize {
end := i + batchSize
if end > totalCount {
end = totalCount
}
// 准备这一批的数据...
c.Insert(ctx, collectionName, "", columns...)
}
c.Flush(ctx, collectionName, false)
分区操作
go
// 创建分区
err := c.CreatePartition(ctx, collectionName, "2024")
// 插入到指定分区
c.Insert(ctx, collectionName, "2024", columns...)
// 在指定分区搜索
c.Search(ctx, collectionName, []string{"2024"}, "", ...)
// 删除分区
c.DropPartition(ctx, collectionName, "2024")
进阶:重排序技术
在实际应用中,可以在向量搜索后添加**重排序(Reranking)**步骤,进一步提升搜索精度。
什么是重排序
重排序是一种两阶段检索(Two-Stage Retrieval)架构:
┌─────────────────────────────────────────────────────────────────────────┐
│ 两阶段检索流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ 用户查询 │ │
│ │ "学Python" │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 第一阶段:向量召回(Retrieval) │ │
│ │ │ │
│ │ 1. 使用 BGE-M3 将查询转换为 1024 维向量 │ │
│ │ 2. 在 Milvus 中执行 ANN 搜索 │ │
│ │ 3. 返回 Top 10 候选结果 │ │
│ │ │ │
│ │ 特点:速度快(毫秒级),可处理百万级数据 │ │
│ └───────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 第二阶段:重排序(Reranking) │ │
│ │ │ │
│ │ 1. 对 Top 10 候选重新计算相关性分数 │ │
│ │ 2. 按相关性分数重新排序 │ │
│ │ 3. 返回最终 Top 5 结果 │ │
│ │ │ │
│ │ 特点:精度更高,提升搜索质量 │ │
│ └───────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 最终结果 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
为什么需要两阶段?
| 对比项 | 仅向量搜索 | 向量搜索 + 重排序 |
|---|---|---|
| 召回量 | 直接返回 Top 5 | 召回 Top 10 → 精排 Top 5 |
| 精度 | 较高 | 更高(提升 5-15%) |
| 速度 | 最快 | 略慢(增加重排序时间) |
| 适用场景 | 对精度要求不高 | RAG、问答系统等对精度敏感场景 |
重排序算法介绍
BM25 算法
BM25(Best Matching 25)是一个经典的文本相关性排序算法,无需模型,纯算法实现。
| 特性 | 说明 |
|---|---|
| 🚀 无需模型 | 纯数学公式,无需下载任何模型 |
| ⚡ 速度极快 | 微秒级计算,无 API 调用开销 |
| 🏆 经典可靠 | 被 Elasticsearch、Lucene 等采用 |
| 🔄 互补效果 | 与向量搜索形成语义+关键词双重匹配 |
BM25 算法原理:
BM25 公式:
score(D, Q) = Σ IDF(qi) × (f(qi,D) × (k1+1)) / (f(qi,D) + k1×(1-b+b×|D|/avgdl))
其中:
• Q: 查询,由多个词 q1, q2, ... 组成
• D: 文档
• f(qi, D): 词 qi 在文档 D 中的出现频率(词频 TF)
• |D|: 文档 D 的长度(词数)
• avgdl: 所有文档的平均长度
• k1, b: 可调参数(通常 k1=1.5, b=0.75)
• IDF(qi): 逆文档频率,衡量词的重要性
Go 实现示例:
go
// BM25Parameters BM25 算法参数
type BM25Parameters struct {
K1 float64 // 词频饱和参数,通常取 1.2-2.0
B float64 // 文档长度归一化参数,通常取 0.75
}
// calculateBM25Score 计算单个文档的 BM25 分数
func calculateBM25Score(queryTokens, docTokens []string, idf map[string]float64,
avgDocLen float64, params BM25Parameters) float64 {
score := 0.0
docLen := float64(len(docTokens))
// 统计文档中每个词的频率
termFreq := make(map[string]int)
for _, token := range docTokens {
termFreq[token]++
}
// 计算每个查询词的贡献
for _, queryToken := range queryTokens {
idfScore := idf[queryToken]
tf := float64(termFreq[queryToken])
if tf == 0 {
continue
}
// BM25 公式
numerator := tf * (params.K1 + 1)
denominator := tf + params.K1*(1-params.B+params.B*docLen/avgDocLen)
score += idfScore * numerator / denominator
}
return score
}
重排序模型介绍
Cross-Encoder 重排序模型
与 BM25 算法不同,重排序模型使用深度学习来计算查询和文档的相关性。
| 模型 | 说明 | 模型大小 |
|---|---|---|
| bge-reranker-v2-m3 | BAAI 开发的多语言重排序模型 | ~1.1GB |
| Qwen3-Reranker | 阿里云开发的重排序模型系列 | 0.6B-8B |
| cohere-rerank | Cohere 的商业重排序 API | - |
BM25 vs 重排序模型
| 对比项 | BM25 算法 | Reranker 模型 |
|---|---|---|
| 依赖 | 无需额外模型 | 需要下载模型(~1GB) |
| 速度 | 极快(微秒级) | 较慢(需模型推理) |
| 资源 | 几乎不占资源 | 需要 CPU/GPU 算力 |
| 效果 | 关键词匹配准确 | 语义理解更深 |
| 适用场景 | 通用场景 | 对语义要求极高的场景 |
Ollama 调用重排序模型示例
go
// 使用 Ollama 调用 Cross-Encoder 重排序模型
func rerankWithModel(query string, docs []string) ([]float64, error) {
scores := make([]float64, len(docs))
for i, doc := range docs {
// 构造 prompt
prompt := fmt.Sprintf("Query: %s\nDocument: %s\nRelevance:", query, doc)
// 调用 Ollama API
resp, err := http.Post(
"http://localhost:11434/api/generate",
"application/json",
bytes.NewBuffer([]byte(fmt.Sprintf(`{
"model": "qllama/bge-reranker-v2-m3",
"prompt": "%s",
"stream": false
}`, prompt))),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 解析响应获取相关性分数...
scores[i] = parseScore(resp)
}
return scores, nil
}
💡 提示:本教程代码中未包含重排序功能,如需使用可参考上述示例自行实现。
进阶主题
1. 切换向量模型
go
// 使用 OpenAI
import "github.com/sashabaranov/go-openai"
client := openai.NewClient("your-api-key")
resp, _ := client.CreateEmbeddings(ctx, openai.EmbeddingRequest{
Model: openai.SmallEmbedding3,
Input: []string{"要转换的文本"},
})
embedding := resp.Data[0].Embedding
// 注意:需要将 VectorDim 改为 1536
2. 连接池与并发
go
// SDK 内部已实现连接池,支持并发操作
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Search(ctx, ...)
}()
}
wg.Wait()
3. 错误处理最佳实践
go
result, err := c.Search(ctx, ...)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
// 超时处理
}
log.Printf("搜索失败: %v", err)
return
}
文章向量化策略
在将文章写入向量库时,需要根据文章长度选择合适的向量化策略。
策略选择标准
┌─────────────────────────────────────────────────────────────────────────────┐
│ 文章向量化策略决策流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ 文章总长度 │ │
│ │ (标题+摘要+内容) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ ≤ 512 字 │ │ 512 ~ 2000 字 │ │ > 2000 字 │ │
│ │ (短文章) │ │ (中等长度) │ │ (长文章) │ │
│ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ 整体向量化 ✓ │ │ 整体向量化 ✓ │ │ 分块向量化 ✓ │ │
│ │ (不拆分) │ │ (可选拆分) │ │ (必须拆分) │ │
│ │ │ │ │ │ │ │
│ │ 1 篇 = 1 向量 │ │ 1 篇 = 1 向量 │ │ 1 篇 = N 向量 │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
长度阈值说明
| 文章长度 | 策略 | 说明 | 适用场景 |
|---|---|---|---|
| ≤ 512 字 | 整体向量化 | 直接将 标题 + 摘要 + 内容 合并生成一个向量 |
新闻摘要、产品描述、FAQ |
| 512 ~ 2000 字 | 整体向量化(推荐) | BGE-M3 支持最长 8192 tokens,2000 字完全可以处理 | 博客文章、技术文档、知识库 |
| > 2000 字 | 分块向量化 | 按段落或固定长度切分,每块独立向量化 | 论文、书籍章节、长篇报告 |
💡 为什么是 512 和 2000 字?
- 512 字:大多数向量模型的最佳输入长度,语义表达最完整
- 2000 字:考虑到中文约 1.5-2 tokens/字,2000 字约 3000-4000 tokens,在 BGE-M3 的 8192 限制内留有余量
策略一:整体向量化(本示例采用)
适用于短篇文章(≤ 2000 字)
go
// 将 标题 + 摘要 + 内容 合并为一个文本,生成单一向量
textForEmbedding := fmt.Sprintf("%s %s %s", article.Title, article.Summary, article.Content)
embedding, err := getArticleEmbedding(textForEmbedding)
优点:
- ✅ 实现简单,每篇文章一条记录
- ✅ 搜索结果直接是完整文章
- ✅ 无需处理分块边界问题
缺点:
- ⚠️ 长文章语义信息可能被稀释
- ⚠️ 无法定位到文章中的具体段落
策略二:分块向量化(Chunking)
适用于长篇文章(> 2000 字)
分块方法对比
| 分块方法 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度分块 | 按字符数切分(如每 500 字) | 实现简单 | 可能切断语义 |
| 段落分块 | 按段落(换行符)切分 | 保持语义完整 | 段落长度不均 |
| 句子分块 | 按句子切分后组合 | 语义边界清晰 | 实现复杂 |
| 重叠分块 | 相邻块有重叠部分 | 避免边界信息丢失 | 存储冗余 |
| 语义分块 | 使用 NLP 识别语义边界 | 效果最好 | 计算成本高 |
推荐:重叠分块实现
go
// ChunkConfig 分块配置
type ChunkConfig struct {
ChunkSize int // 每块目标大小(字符数)
ChunkOverlap int // 重叠大小(字符数)
}
// 推荐配置
var DefaultChunkConfig = ChunkConfig{
ChunkSize: 500, // 每块约 500 字
ChunkOverlap: 50, // 重叠 50 字(10%)
}
// splitArticleToChunks 将长文章拆分为多个块
func splitArticleToChunks(content string, config ChunkConfig) []string {
runes := []rune(content)
var chunks []string
for i := 0; i < len(runes); {
end := i + config.ChunkSize
if end > len(runes) {
end = len(runes)
}
chunk := string(runes[i:end])
chunks = append(chunks, chunk)
// 下一块起始位置 = 当前位置 + 块大小 - 重叠大小
i += config.ChunkSize - config.ChunkOverlap
if end == len(runes) {
break
}
}
return chunks
}
// 使用示例
func insertLongArticle(article Article) {
// 判断是否需要分块
totalLen := len([]rune(article.Title + article.Summary + article.Content))
if totalLen <= 2000 {
// 短文章:整体向量化
text := fmt.Sprintf("%s %s %s", article.Title, article.Summary, article.Content)
embedding := getArticleEmbedding(text)
// 插入单条记录...
} else {
// 长文章:分块向量化
chunks := splitArticleToChunks(article.Content, DefaultChunkConfig)
for i, chunk := range chunks {
// 每个块都带上标题信息,增强语义关联
text := fmt.Sprintf("%s - 第%d部分: %s", article.Title, i+1, chunk)
embedding := getArticleEmbedding(text)
// 插入每个块,记录 chunk_index...
}
}
}
分块后的数据结构
对于长文章分块,建议扩展 Collection Schema:
go
// 分块文章的 Schema 字段
{
Name: "chunk_index",
DataType: entity.FieldTypeInt32,
// 块索引:0, 1, 2, ...
},
{
Name: "chunk_total",
DataType: entity.FieldTypeInt32,
// 总块数
},
{
Name: "article_uuid",
DataType: entity.FieldTypeVarChar,
TypeParams: map[string]string{"max_length": "64"},
// 原文章唯一标识,用于关联同一篇文章的所有块
},
策略选择总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ 向量化策略最佳实践 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📌 短文章(≤ 512 字) │
│ └─→ 整体向量化,简单高效 │
│ │
│ 📌 中等文章(512 ~ 2000 字) │
│ └─→ 整体向量化(推荐),BGE-M3 完全能处理 │
│ └─→ 如需精准定位段落,可选择分块 │
│ │
│ 📌 长文章(> 2000 字) │
│ └─→ 必须分块向量化 │
│ └─→ 推荐 500 字/块 + 50 字重叠 │
│ └─→ 每块带上标题信息增强关联 │
│ │
│ 📌 特殊场景 │
│ └─→ 问答系统:分块 + 按相关性返回具体段落 │
│ └─→ 文章推荐:整体向量化 + 返回完整文章 │
│ └─→ 知识库检索:分块 + 支持上下文扩展 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 场景 | 推荐策略 | 块大小 | 重叠 |
|---|---|---|---|
| 新闻/摘要 | 整体向量化 | - | - |
| 博客文章 | 整体向量化 | - | - |
| 技术文档 | 分块(可选) | 500 字 | 50 字 |
| 论文/书籍 | 分块(必须) | 500 字 | 100 字 |
| FAQ 问答 | 整体向量化 | - | - |
| 法律合同 | 分块 + 段落保持 | 按段落 | 1-2 句 |
Milvus 高可用架构
Milvus 通过分布式架构设计 和多重容错机制来保证高可用性。
四层架构分离
Milvus 采用分层架构,各组件职责分离,减少单点故障:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Milvus 分布式架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 访问层 (Access Layer) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Proxy │ │ Proxy │ │ Proxy │ ← 无状态,水平扩展 │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └────────┼────────────┼────────────┼──────────────────────────────────┘ │
│ │ │ │ │
│ ┌────────┴────────────┴────────────┴──────────────────────────────────┐ │
│ │ 协调者层 (Coordinator) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │RootCoord │ │QueryCoord│ │DataCoord │ │IndexCoord│ │ │
│ │ │ (Active) │ │ (Active) │ │ (Active) │ │ (Active) │ │ │
│ │ │ Standby │ │ Standby │ │ Standby │ │ Standby │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ └────────┼─────────────┼─────────────┼─────────────┼──────────────────┘ │
│ │ │ │ │ │
│ ┌────────┴─────────────┴─────────────┴─────────────┴──────────────────┐ │
│ │ 工作节点层 (Worker Nodes) │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ QueryNode │ │ DataNode │ │ IndexNode │ │ │
│ │ │ x N │ │ x N │ │ x N │ │ │
│ │ │ (搜索查询) │ │ (数据写入) │ │ (索引构建) │ │ │
│ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │
│ └─────────┼──────────────────┼──────────────────┼─────────────────────┘ │
│ │ │ │ │
│ ┌─────────┴──────────────────┴──────────────────┴─────────────────────┐ │
│ │ 存储层 (Storage) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ etcd │ │ MinIO │ │ Pulsar │ │ │
│ │ │ (元数据) │ │ (对象存储) │ │ (消息队列) │ │ │
│ │ │ 3+ 副本 │ │ 分布式冗余 │ │ WAL 日志 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 层级 | 组件 | 职责 | HA 特性 |
|---|---|---|---|
| 访问层 | Proxy | 请求入口、验证、结果汇聚 | 无状态,水平扩展 |
| 协调者层 | Coordinator | 集群拓扑、任务调度、元数据管理 | Active-Standby 模式 |
| 工作节点层 | DataNode / QueryNode / IndexNode | 数据处理、搜索、索引构建 | 无状态,水平扩展 |
| 存储层 | etcd / MinIO / Pulsar | 元数据 / 对象存储 / 日志 | 分布式冗余存储 |
工作节点详解
QueryNode(查询节点)
职责:执行向量搜索和标量查询
| 特性 | 说明 |
|---|---|
| 核心功能 | 向量相似度搜索(ANN)、标量查询 |
| 数据来源 | 从对象存储加载 Segment 到内存 |
| 状态 | 有状态(内存中缓存数据) |
| 扩展性 | 水平扩展,更多节点 = 更大吞吐量 |
DataNode(数据节点)
职责:处理数据写入和持久化
| 特性 | 说明 |
|---|---|
| 核心功能 | 数据插入、更新、删除、持久化 |
| 工作方式 | 订阅消息队列,消费写入日志 |
| 输出 | Segment 文件(存储到 MinIO/S3) |
| 状态 | 局部状态,可水平扩展 |
IndexNode(索引节点)
职责:构建向量索引
| 特性 | 说明 |
|---|---|
| 核心功能 | 构建向量索引(IVF_FLAT、HNSW 等) |
| 触发时机 | 数据持久化后,由 IndexCoord 调度 |
| 输出 | 索引文件(加速搜索) |
| 状态 | 无状态,任务完成即释放 |
三者协作流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 数据生命周期流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 插入数据 │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Insert │ → │ Proxy │ → │ DataNode │ → │ MinIO │ │
│ │ 请求 │ │ 路由 │ │ 持久化 │ │ Segment │ │
│ └──────────┘ └──────────┘ └───────────┘ └──────────┘ │
│ │
│ 2. 创建索引 │
│ ┌──────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Segment │ → │IndexCoord │ → │ IndexNode │ → │ MinIO │ │
│ │ 文件 │ │ 调度 │ │ 构建索引 │ │ Index文件│ │
│ └──────────┘ └───────────┘ └───────────┘ └──────────┘ │
│ │
│ 3. 加载到内存 │
│ ┌──────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Load │ → │QueryCoord │ → │ QueryNode │ (Segment + Index) │
│ │ 请求 │ │ 调度 │ │ 加载内存 │ │
│ └──────────┘ └───────────┘ └───────────┘ │
│ │
│ 4. 执行搜索 │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Search │ → │ Proxy │ → │ QueryNode │ → │ 结果 │ │
│ │ 请求 │ │ 路由 │ │ ANN 搜索 │ │ 返回 │ │
│ └──────────┘ └──────────┘ └───────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
协调者高可用:Active-Standby 模式
协调者是集群的"大脑",Milvus 2.2.3+ 引入了 Active-Standby 机制:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Coordinator Active-Standby 机制 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ etcd (租约机制) │ │
│ │ ┌───────────────┐ │ │
│ │ │ Lease 租约 │ │ │
│ │ └───────┬───────┘ │ │
│ └────────────┼────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Coordinator │ │ Coordinator │ │
│ │ (Active) ✓ │ │ (Standby) ⏳ │ │
│ │ │ │ │ │
│ │ • 处理请求 │ │ • 监听租约状态 │ │
│ │ • 更新元数据 │ │ • 等待接管 │ │
│ │ • 任务调度 │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ Active 失效 │ │
│ │ ─────────────────────────────► │ │
│ │ │ │
│ │ ┌────────┴─────────┐ │
│ │ │ Coordinator │ │
│ ▼ │ (New Active) ✓ │ │
│ ┌──────────────────┐ │ │ │
│ │ (Failed) ✗ │ │ • 从 etcd 加载 │ │
│ └──────────────────┘ │ 最新元数据 │ │
│ │ • 接管服务 │ │
│ └──────────────────┘ │
│ │
│ 切换时间 ≈ etcd lease timeout(默认 60 秒) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
- 多个 Coordinator 实例同时运行,仅一个为 Active
- 使用 etcd 租约(lease)机制进行选主
- Active 失效后,Standby 自动接管(切换时间约 60 秒)
元数据与日志保障
| 组件 | 作用 | HA 机制 |
|---|---|---|
| etcd | 存储元数据、Schema、节点状态 | 分布式一致性,强一致读写,建议 3+ 副本 |
| WAL (Write Ahead Log) | 所有写操作先写日志 | 故障恢复时重放日志,支持 Pulsar/Kafka |
| 对象存储 | 持久化向量数据、索引文件 | MinIO/S3/Azure Blob 自带冗余 |
故障恢复时间
| 故障类型 | 恢复机制 | 预计恢复时间 |
|---|---|---|
| Proxy/QueryNode 故障 | K8s Pod 重启 | 几秒 ~ 几十秒 |
| Coordinator 故障 | Active-Standby 切换 | ~60 秒(etcd 租约超时) |
| etcd 单节点故障 | Raft 自动选主 | 几秒 |
| 存储故障 | 依赖 MinIO/S3 冗余 | 取决于存储系统 |
生产环境高可用部署
推荐使用 Kubernetes + Milvus Operator 部署:
yaml
# Milvus 高可用集群配置示例
apiVersion: milvus.io/v1beta1
kind: Milvus
metadata:
name: milvus-cluster
spec:
mode: cluster
# 依赖组件配置
dependencies:
etcd:
inCluster:
values:
replicaCount: 3 # etcd 3 副本
persistence:
enabled: true
storageClass: "fast-ssd"
storage:
inCluster:
values:
mode: distributed # MinIO 分布式模式
replicas: 4
pulsar:
inCluster:
values:
components:
autorecovery: true
# Milvus 组件配置
components:
# 访问层
proxy:
replicas: 2 # Proxy 2 副本
resources:
requests:
cpu: "1"
memory: "2Gi"
# 工作节点
queryNode:
replicas: 3 # QueryNode 3 副本
resources:
requests:
cpu: "2"
memory: "8Gi"
dataNode:
replicas: 2
indexNode:
replicas: 2
# 协调者(开启 Active-Standby)
rootCoord:
replicas: 2 # 1 Active + 1 Standby
queryCoord:
replicas: 2
dataCoord:
replicas: 2
indexCoord:
replicas: 2
高可用最佳实践
| 实践项 | 建议 | 说明 |
|---|---|---|
| etcd 副本数 | ≥ 3 | 保证元数据存储可靠性 |
| Coordinator 副本 | ≥ 2 | 启用 Active-Standby |
| QueryNode 副本 | ≥ 2 | 查询高可用 |
| 对象存储 | 使用云存储或分布式 MinIO | 数据持久化可靠性 |
| 监控告警 | 配置 Prometheus + Grafana | 及时发现问题 |
| 定期备份 | 使用 Milvus Backup 工具 | 灾难恢复 |
高可用设计总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ Milvus 高可用核心设计 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ 1. 分层解耦 │ 各层独立扩展,互不影响 │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ 2. 无状态设计 │ Proxy/DataNode/IndexNode 可随意扩缩容 │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ 3. Active-Standby │ 协调者故障自动切换 │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ 4. WAL + etcd │ 数据一致性和故障恢复保障 │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ 5. 分布式存储 │ etcd、MinIO 多副本冗余 │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
参考资源
Milvus
BGE 模型系列
Ollama
- 🏠 Ollama 官网
- 📦 Ollama 模型库
- 🐙 Ollama GitHub
许可证
MIT License