RAG 性能总卡在检索?先搞懂 Milvus 向量数据库再说
在构建 Agent 记忆模块时,我完整走了一遍 Milvus 的接入流程。本文以 AI 日记项目为例,梳理其中的关键技术点。
一、向量数据库解决什么问题
传统数据库靠关键词精确匹配,搜"户外活动"找不到"爬山""公园散步"这类语义相关的内容。向量数据库的做法是将文本通过 Embedding 模型转为高维向量,以向量距离衡量语义相似度,从而实现"以义搜义"。
在 RAG 架构中,向量数据库承担知识检索环节,是 AI Agent 实现长期记忆的基础。
核心概念映射
| 概念 | 类比 | 说明 |
|---|---|---|
| Collection | Table | 数据表 |
| Entity | Row | 一条记录 |
| Field | Column | 字段,标量或向量类型 |
| Partition | 分区表 | 逻辑隔离,用于多租户/按时间归档 |
| Index | Index | 加速 ANN(近似最近邻)搜索 |
二、环境准备
bash
pnpm add @zilliz/milvus2-sdk-node @langchain/openai dotenv
js
import { MilvusClient } from '@zilliz/milvus2-sdk-node';
import { OpenAIEmbeddings } from '@langchain/openai';
// 以阿里通义 text-embedding-v2 为例,维度 1536
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: 'text-embedding-v2',
configuration: { baseURL: process.env.OPENAI_BASE_URL },
dimensions: 1536,
});
const client = new MilvusClient({
address: process.env.MILVUS_ADDRESS,
token: process.env.MILVUS_TOKEN,
maxRetries: 3,
timeout: 30000,
});
async function getEmbeddings(text) {
return await embeddings.embedQuery(text);
}
Zilliz Cloud 免运维开箱即用;自托管则通过 Docker Compose 一键启动,默认地址 localhost:19530,无需 token。
三、Schema 设计与 Collection 管理
3.1 字段类型
| 类型 | 使用场景 |
|---|---|
VarChar |
id, content, date, mood |
Int64 / Float / Bool |
数值及布尔标量 |
JSON |
灵活的元数据 |
Array |
标签 tags |
FloatVector |
content 的 Embedding |
BinaryVector / SparseFloatVector |
图像指纹 / 关键词稀疏向量 |
3.2 创建 AI 日记 Collection
js
await client.createCollection({
collection_name: 'ai_diary',
fields: [
{ name: 'id', data_type: DataType.VarChar, max_length: 50, is_primary_key: true },
{ name: 'vector', data_type: DataType.FloatVector, dim: 1536 },
{ name: 'content', data_type: DataType.VarChar, max_length: 5000 },
{ name: 'date', data_type: DataType.VarChar, max_length: 50 },
{ name: 'mood', data_type: DataType.VarChar, max_length: 50 },
{ name: 'tags', data_type: DataType.Array, element_type: DataType.VarChar, max_capacity: 10, max_length: 50 },
],
});
Milvus 也支持动态 Schema (enableDynamicField: true),未声明字段自动归入 JSON 列,适合原型快速迭代,生产建议切回强 Schema。
3.3 生命周期操作
js
await client.hasCollection({ collection_name: 'ai_diary' });
await client.describeCollection({ collection_name: 'ai_diary' });
await client.showCollections();
await client.alterCollection({ collection_name: 'ai_diary', properties: { 'collection.ttl.seconds': '604800' } });
await client.dropCollection({ collection_name: 'ai_diary' });
3.4 Partition 分区
js
await client.createPartition({ collection_name: 'ai_diary', partition_name: '2026_01' });
await client.insert({ collection_name: 'ai_diary', partition_name: '2026_01', data: [...] });
await client.search({ collection_name: 'ai_diary', partition_names: ['2026_01'], vector, limit: 10 });
限定分区搜索可大幅缩小候选集,显著提升 QPS。
四、向量索引------性能的核心
4.1 索引选型对比
| 索引 | 原理 | 精度 | 内存 | 适用规模 |
|---|---|---|---|---|
| FLAT | 暴力搜索 | 100% | 低 | <10 万 |
| IVF_FLAT | K-Means + 候选集暴搜 | >95% | 中 | 百万级 |
| IVF_SQ8 | IVF + 8-bit 量化 | >92% | 低(4×压缩) | 百万级,内存受限 |
| IVF_PQ | IVF + 乘积量化 | >85% | 极低(16×) | 千万级 |
| HNSW | 分层图搜索 | >98% | 高 | 百万级,高 QPS |
| DISKANN | 基于 SSD | >95% | 磁盘 | 亿级 |
4.2 索引创建
js
// 默认选择:IVF_FLAT + COSINE
await client.createIndex({
collection_name: 'ai_diary',
field_name: 'vector',
index_type: 'IVF_FLAT',
metric_type: 'COSINE',
params: { nlist: 1536 },
});
// HNSW------高召回高QPS,内存换性能
await client.createIndex({
collection_name: 'ai_diary', field_name: 'vector',
index_type: 'HNSW', metric_type: 'COSINE',
params: { M: 16, efConstruction: 200 },
});
// IVF_SQ8------向量压缩到 1/4
await client.createIndex({
collection_name: 'ai_diary', field_name: 'vector',
index_type: 'IVF_SQ8', metric_type: 'COSINE',
params: { nlist: 1024 },
});
决策逻辑:<10 万用 FLAT;百万级默认 IVF_FLAT,内存受限用 IVF_SQ8 或 IVF_PQ,高 QPS 用 HNSW;亿级走 DISKANN。
4.3 相似度度量
| 方式 | 公式 | 适用 |
|---|---|---|
| COSINE | 余弦相似度 | 文本语义(最常用) |
| L2 | 欧几里得距离 | 未归一化向量 |
| IP | 内积 | 归一化向量(计算最快) |
| HAMMING / JACCARD | 汉明/杰卡德 | BinaryVector |
注意:如果向量已 L2 归一化,IP 与 COSINE 数学等价,IP 计算开销更小。本项目的 text-embedding-v2 未做归一化,故用 COSINE。
五、数据操作
插入
js
const diaryData = [
{ id: 'diary_001', content: '今天天气很好,去公园散步了...', date: '2026-01-10', mood: 'happy', tags: ['生活', '散步'] },
{ id: 'diary_002', content: '今天工作很忙,完成了一个重要的项目里程碑...', date: '2026-01-11', mood: 'excited', tags: ['工作', '成就'] },
{ id: 'diary_003', content: '周末和朋友去爬山,天气很好...', date: '2026-01-12', mood: 'relaxed', tags: ['户外', '朋友'] },
{ id: 'diary_004', content: '今天学习了Milvus向量数据库...', date: '2026-01-12', mood: 'curious', tags: ['学习', '技术'] },
{ id: 'diary_005', content: '晚上做了一顿丰盛的晚餐...', date: '2026-01-13', mood: 'proud', tags: ['美食', '家庭'] },
];
const data = await Promise.all(diaryData.map(async d => ({ ...d, vector: await getEmbeddings(d.content) })));
const { insert_cnt } = await client.insert({ collection_name: 'ai_diary', data });
Upsert / Query / Delete
js
// Upsert:存在即更新,不存在即插入
await client.upsert({ collection_name: 'ai_diary', data: [{ id: 'diary_001', vector: newVec, content: '...' }] });
// Query:标量条件精确匹配,不走向量
await client.query({ collection_name: 'ai_diary', expr: 'mood == "happy"', output_fields: ['id', 'content'], limit: 100 });
// Delete
await client.delete({ collection_name: 'ai_diary', expr: 'id in ["diary_003"]' });
await client.delete({ collection_name: 'ai_diary', expr: 'date < "2026-01-01"' });
Query vs Search:Query 对标量做精确过滤,速度快;Search 做向量 ANN 搜索,能匹配语义相近的结果。
六、向量搜索
基础语义搜索
js
await client.loadCollection({ collection_name: 'ai_diary' });
const query = '我想看看关于户外活动的日记';
const queryVector = await getEmbeddings(query);
const { results } = await client.search({
collection_name: 'ai_diary',
vector: queryVector,
limit: 3,
metric_type: 'COSINE',
output_fields: ['id', 'content', 'date', 'mood', 'tags'],
});
搜索"户外活动"会优先返回 diary_003(爬山)和 diary_001(公园散步),尽管原文中并不包含"户外活动"这个词。
标量过滤搜索
js
// 向量 + 条件组合,生产中的标准用法
await client.search({
collection_name: 'ai_diary',
vector: queryVector,
limit: 10,
filter: 'array_contains(tags, "户外") and date >= "2026-01-10"',
output_fields: ['id', 'content', 'tags'],
});
支持的表达式:==, !=, >, >=, <, <= / and, or, not / in [...] / like "prefix%" / array_contains() / metadata["key"]。
范围搜索与分页
js
// 按相似度阈值搜索
await client.search({ ..., params: { radius: 0.8, range_filter: 0.5 } });
// 分页
await client.search({ ..., limit: 10, offset: 0 });
await client.search({ ..., limit: 10, offset: 10 });
多向量混合搜索(2.4+)
同时用稠密向量(语义)和稀疏向量(关键词)做多路召回,RRF 重排序:
js
await client.hybridSearch({
collection_name: 'ai_diary',
searches: [
{ vector: denseVec, anns_field: 'dense_vector', metric_type: 'COSINE', limit: 100 },
{ vector: sparseVec, anns_field: 'sparse_vector', metric_type: 'IP', limit: 100 },
],
rerank: { strategy: 'rrf', params: { k: 60 } },
limit: 10,
output_fields: ['id', 'content'],
});
七、内存管理与一致性
Collection 必须 load 进 QueryNode 内存才能被检索,用毕 release 释放:
js
await client.loadCollection({ collection_name: 'ai_diary', replica_number: 2 });
await client.releaseCollection({ collection_name: 'ai_diary' });
四种一致性级别:
| 级别 | 语义 | 场景 |
|---|---|---|
| Strong | 线性一致,保证读到最新 | 金融、库存 |
| Session | 同客户端 read-your-writes | 写后即查 |
| Bounded | 容忍指定窗口内的延迟 | 一般业务 |
| Eventually | 最终一致,可能陈旧 | 推荐、分析 |
js
await client.insert({ ..., consistency_level: 'Session' });
八、生产要点
错误重试 :SDK 内置 maxRetries,业务层建议再兜一层 rate_limit 的退避重试。
批量写入 :每批 1000 条分片提交,最后显式 flush 确保落盘。
模型选型:
| 模型 | 维度 | 特点 |
|---|---|---|
| text-embedding-3-small (OpenAI) | 1536 | 通用均衡 |
| text-embedding-v2 (阿里) | 1536 | 中文语义强 |
| BGE-M3 (BAAI) | 1024 | 开源,稠密+稀疏 |
| M3E-large | 1024 | 开源中文轻量 |
选定后不要轻易更换------换模型意味着所有向量重新入库。
成本控制:维度选 1536 即可;非核心场景用 IVF_SQ8 压缩 75% 内存;按时间分区 + TTL 自动清理;冷数据 release、热数据常驻。
九、总结
本文以 AI 日记项目为线索,覆盖了 Milvus 从接入到生产的核心链路:
- Schema:字段类型、强/动态 Schema、Partition 分区
- 索引:FLAT → IVF_FLAT → HNSW → IVF_SQ8 → IVFPQ → DISKANN 的递进路径
- 操作:Insert / Upsert / Query / Delete
- 搜索:语义搜索、标量过滤、范围搜索、分页、多向量混合+RFF
- 可靠性:一致性级别与内存管理
- 工程:重试、批量写入、模型选型、成本优化
向量数据库是 RAG 的记忆基础,搞懂 Milvus 就搞懂了 AI 检索链路的底层逻辑。