RAG 性能总卡在检索?先搞懂 Milvus 向量数据库再说

285 阅读7分钟

RAG 性能总卡在检索?先搞懂 Milvus 向量数据库再说

在构建 Agent 记忆模块时,我完整走了一遍 Milvus 的接入流程。本文以 AI 日记项目为例,梳理其中的关键技术点。


一、向量数据库解决什么问题

传统数据库靠关键词精确匹配,搜"户外活动"找不到"爬山""公园散步"这类语义相关的内容。向量数据库的做法是将文本通过 Embedding 模型转为高维向量,以向量距离衡量语义相似度,从而实现"以义搜义"。

在 RAG 架构中,向量数据库承担知识检索环节,是 AI Agent 实现长期记忆的基础。

核心概念映射

概念类比说明
CollectionTable数据表
EntityRow一条记录
FieldColumn字段,标量或向量类型
Partition分区表逻辑隔离,用于多租户/按时间归档
IndexIndex加速 ANN(近似最近邻)搜索

二、环境准备

pnpm add @zilliz/milvus2-sdk-node @langchain/openai dotenv
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 字段类型

类型使用场景
VarCharid, content, date, mood
Int64 / Float / Bool数值及布尔标量
JSON灵活的元数据
Array标签 tags
FloatVectorcontent 的 Embedding
BinaryVector / SparseFloatVector图像指纹 / 关键词稀疏向量

3.2 创建 AI 日记 Collection

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 也支持动态 SchemaenableDynamicField: true),未声明字段自动归入 JSON 列,适合原型快速迭代,生产建议切回强 Schema。

3.3 生命周期操作

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 分区

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_FLATK-Means + 候选集暴搜>95%百万级
IVF_SQ8IVF + 8-bit 量化>92%低(4×压缩)百万级,内存受限
IVF_PQIVF + 乘积量化>85%极低(16×)千万级
HNSW分层图搜索>98%百万级,高 QPS
DISKANN基于 SSD>95%磁盘亿级

4.2 索引创建

// 默认选择: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。


五、数据操作

插入

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

// 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 搜索,能匹配语义相近的结果。


六、向量搜索

基础语义搜索

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(公园散步),尽管原文中并不包含"户外活动"这个词。

标量过滤搜索

// 向量 + 条件组合,生产中的标准用法
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"]

范围搜索与分页

// 按相似度阈值搜索
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 重排序:

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 释放:

await client.loadCollection({ collection_name: 'ai_diary', replica_number: 2 });
await client.releaseCollection({ collection_name: 'ai_diary' });

四种一致性级别:

级别语义场景
Strong线性一致,保证读到最新金融、库存
Session同客户端 read-your-writes写后即查
Bounded容忍指定窗口内的延迟一般业务
Eventually最终一致,可能陈旧推荐、分析
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-large1024开源中文轻量

选定后不要轻易更换——换模型意味着所有向量重新入库。

成本控制:维度选 1536 即可;非核心场景用 IVF_SQ8 压缩 75% 内存;按时间分区 + TTL 自动清理;冷数据 release、热数据常驻。


九、总结

本文以 AI 日记项目为线索,覆盖了 Milvus 从接入到生产的核心链路:

  1. Schema:字段类型、强/动态 Schema、Partition 分区
  2. 索引:FLAT → IVF_FLAT → HNSW → IVF_SQ8 → IVFPQ → DISKANN 的递进路径
  3. 操作:Insert / Upsert / Query / Delete
  4. 搜索:语义搜索、标量过滤、范围搜索、分页、多向量混合+RFF
  5. 可靠性:一致性级别与内存管理
  6. 工程:重试、批量写入、模型选型、成本优化

向量数据库是 RAG 的记忆基础,搞懂 Milvus 就搞懂了 AI 检索链路的底层逻辑。