一、为什么需要向量数据库?
传统关系型数据库(如 MySQL)擅长精确匹配查询——"查找日期为 2026-01-12 的日记",一条 WHERE date = '2026-01-12' 即可搞定。但当我们想搜索"最近有什么让我感到快乐的事情"时,SQL 就束手无策了。这种语义搜索需要理解自然语言的含义,而不是简单的关键词匹配。
这就是向量数据库(Vector Database)的用武之地。向量数据库的核心思想是:将文本、图片等非结构化数据通过嵌入模型(Embedding Model)转换为高维向量(一组浮点数),语义相近的内容在向量空间中的距离也更近。查询时,将用户问题同样转为向量,然后在高维空间中寻找最邻近的向量——这便是近似最近邻搜索(ANN Search)。
Milvus 是目前最流行的开源向量数据库之一,而 Zilliz Cloud 是其全托管云服务。本文基于一个真实的 Demo 项目,逐步拆解如何使用 Milvus + LangChain + OpenAI 兼容 API 构建一个AI 日记助手,涵盖向量入库、语义搜索和 RAG(检索增强生成)三大核心能力。
二、项目概览与环境准备
2.1 项目结构
Demo 位于 milvus-demo/demo/src/ 目录下,包含四个模块文件,由浅入深地展示了 Milvus 的核心用法:
文件
功能
main.mjs
基础入门:连接 Milvus、创建 Collection、建索引、插入与搜索
query.mjs
工具模块:初始化 Embeddings 客户端和 Milvus 连接
index.mjs
数据入库:定义自定义 Schema、批量生成向量、写入日记数据、执行语义搜索
rag.mjs
RAG 应用:检索相关日记 + 大模型生成温暖回复,实现智能问答
2.2 依赖与配置
项目使用 ES Module(.mjs),核心依赖包括:
@zilliz/milvus2-sdk-node(v3.x):Milvus 的 Node.js SDK,提供 Collection 管理、向量搜索等全部 API。@langchain/openai:LangChain 的 OpenAI 集成,封装了OpenAIEmbeddings(文本转向量)和ChatOpenAI(大模型对话)。dotenv:管理环境变量,保护 API 密钥。
环境变量配置(.env)包含 Zilliz Cloud 的连接地址与 Token、OpenAI 兼容 API 的 Key 与 Base URL、以及 Embedding 模型名称。
MILVUS_ADDRESS=https://<your-instance>.zillizcloud.com:19530
MILVUS_TOKEN=<your-zilliz-token>
OPENAI_API_KEY=<your-api-key>
OPENAI_BASE_URL=https://api.openai.com/v1
EMBEDDINGS_MODEL_NAME=text-embedding-3-small
MODEL_NAME=gpt-4o-mini
三、第一步:连接 Milvus 与基础概念(main.mjs)
main.mjs 是入门示例,展示了最精简的 Milvus 操作流程。
3.1 建立连接
const client = new MilvusClient({
address: ADDRESS,
token: TOKEN
});
const checkHealth = await client.checkHealth();
v3 SDK 采用自动连接机制,实例化 MilvusClient 后即可直接调用 API。checkHealth() 用于验证集群状态,返回 { isHealthy: true/false, reasons: [...] }。
3.2 Collection:向量数据库的"表"
Collection 是 Milvus 中数据的组织单元,类似于关系数据库中的 Table。创建 Collection 时需要定义:
- 维度(dimension):向量的长度,由 Embedding 模型决定。例如 OpenAI 的
text-embedding-3-small支持 512 或 1536 维。 - 主键:每条记录的唯一标识,可手动指定或
auto_id: true自动生成。 - 自动 Schema 模式:v3 SDK 支持简化创建——只传
dimension和auto_id,Milvus 自动配置默认字段。这适合快速原型开发。
3.3 索引:加速向量搜索的关键
向量搜索的本质是在海量高维向量中找到与查询向量最相似的那批向量。没有索引时(暴力搜索),复杂度为 O(n)——每条向量都要计算一遍距离,数据量一大就慢得无法使用。
Milvus 支持多种索引类型,Demo 中涉及两种:
- AUTOINDEX:让 Milvus 根据数据特征自动选择最优索引策略,适合不熟悉索引参数的用户。
- IVF_FLAT:基于聚簇的索引,先将向量空间划分为若干簇,搜索时只检索引擎判定最相关的几个簇,将计算量从"全库扫描"降低为"候选簇扫描",实现毫秒级响应。
相似度度量(Metric Type)选择 COSINE(余弦相似度),适合文本语义比较场景——它衡量的是向量方向的接近程度,而非绝对距离。
四、第二步:定义 Schema 与数据入库(index.mjs)
index.mjs 构建了一个完整的AI 日记应用,展示了自定义 Schema、批量生成 Embedding、数据写入和语义搜索的全流程。
4.1 自定义 Schema 设计
与实际应用更贴近的做法是自定义 Schema,而非使用自动模式。Demo 为日记定义了六个字段:
fields: [
{ name: 'id', data_type: DataType.VarChar, max_length: 50, is_primary_key: true },
{ name: 'vector', data_type: DataType.FloatVector, dim: 1024 },
{ 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 },
]
这里有几个关键设计:
- id:
VarChar类型主键,手动指定如diary_001,便于追踪和调试。 - vector:
FloatVector类型,1024 维。这是 Embedding 模型决定的维度——Demo 使用的是支持自定义维度的 OpenAI 兼容模型,指定dimensions: 1024。 - content:存储日记正文,
max_length: 5000足够容纳较长的日记内容。 - date / mood:结构化元数据字段,支持后续的过滤查询和结果展示。
- tags:
Array类型,是 Milvus 对复杂数据结构的支持,每个日记可以有多个标签(如['生活', '散步']),最多 10 个。
Schema 设计的核心原则:将需要作为查询结果返回的、或需要用于过滤的字段定义为独立字段;纯粹的全文内容可以存入一个大字段并在创建索引时忽略。
4.2 批量生成 Embedding
const getEmbedding = async (text) => {
const result = await embeddings.embedQuery(text);
return result;
};
const diaryData = await Promise.all(
diaryContents.map(async (diary) => ({
...diary,
vector: await getEmbedding(diary.content)
}))
);
使用 Promise.all 并行处理,对每条日记的 content 调用 embeddings.embedQuery() 生成 1024 维向量。OpenAIEmbeddings 封装了与 OpenAI 兼容 API 的交互,自动处理请求构造和响应解析。
4.3 数据写入与语义搜索
await client.upsert({
collection_name: COLLECTION_NAME,
data: diaryData
});
upsert(update or insert)是一个幂等操作:如果记录已存在则更新,否则插入。这比先查后写的方式更简洁高效。
搜索时,将查询文本"学习新技术的体验"转为向量,然后调用 client.search():
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
data: [queryVector],
limit: 3,
output_fields: ['content', 'date', 'mood', 'tags']
});
limit: 3 指定返回最相似的 3 条结果,output_fields 控制返回哪些字段。返回结果中每一条都包含 score(相似度分数,COSINE 下越接近 1 越相似)和指定的输出字段。
五、第三步:RAG——当向量搜索遇见大模型(rag.mjs)
rag.mjs 是整个 Demo 的精华,它将向量检索与 LLM 生成结合,实现了真正的检索增强生成(RAG, Retrieval-Augmented Generation)。
5.1 RAG 的工作流程
RAG 的核心思想是:先检索,再生成。流程如下:
- 用户提问(如"我最近做了什么让我感到快乐的事情")
- 将问题向量化,在 Milvus 中检索最相关的日记片段
- 将检索到的内容作为上下文,拼接进精心设计的 Prompt
- 大模型基于上下文生成回答,而不是凭空"编造"
5.2 检索模块
async function retrieveRelavantDiaries(question, k = 2) {
const queryVector = await getEmbedding(question);
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
data: [queryVector],
metric_type: MetricType.COSINE,
limit: k,
output_fields: ['id', 'content', 'date', 'mood', 'tags']
});
return searchResult.results;
}
这个函数封装了"问题 → 向量 → 搜索 → 结果"的完整检索管线。k 参数控制召回数量——太少可能遗漏关键信息,太多可能引入噪声。Demo 中设为 2,适合日记这种个人化场景。
5.3 Prompt 工程
RAG 效果的优劣,一半在检索,一半在 Prompt。Demo 的 Prompt 设计非常讲究:
你是一个温暖贴心的AI日记助手,基于用户的日记内容回答问题,用亲切自然的语言。
请根据以下日记内容回答问题:
${context}
用户问题:${question}
回答要求:
1. 如果日记中有相关信息,请结合日记内容给出详细温暖的回答
2. 可以总结多篇日记的内容,找出共同点或趋势
3. 如果日记中没有相关信息,请温和告知用户
4. 用第一人称"你"来称呼日记的作者
5. 回答要有同理心,让用户感到被理解和关心
这个 Prompt 有四个精妙之处:
- 角色设定:"温暖贴心的 AI 日记助手"——定义了助手的语气和人格。
- 上下文注入:
${context}是检索到的日记原文,是回答的事实依据。 - 行为边界:"如果日记中没有相关信息,请温和告知"——防止模型胡编乱造(幻觉)。
- 人称策略:"用第一人称'你'来称呼日记的作者"——消解了"AI 在分析你的日记"的距离感。
5.4 生成回答
const model = new ChatOpenAI({
temperature: 0.1,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL }
});
const response = await model.invoke(prompt);
temperature: 0.1 是一个关键参数——低温度意味着输出更确定、更聚焦于给定上下文,减少自由发挥导致的偏离事实。对于 RAG 场景,低温度几乎是标准配置。
六、四个文件的演进逻辑
回顾四个文件的设计,它们形成了一个清晰的学习曲线和工程演进:
阶段
文件
做了什么
学到什么
1
main.mjs
连接 → 建 Collection → 建索引 → 搜索
Milvus 的最小可用原型,理解 Collection、Index、Search 基础概念
2
query.mjs
提取公共模块(Embeddings 客户端 + Milvus 客户端)
关注点分离,为后续复用做准备
3
index.mjs
自定义 Schema → 批量 Embedding → 数据写入 → 语义搜索
生产级 Schema 设计,批量数据处理的正确姿势
4
rag.mjs
检索 + LLM 生成 → RAG 完整闭环
从"搜到什么返回什么"升级为"搜到什么、理解后回答什么"
这个演进路径值得借鉴:先用最简代码跑通核心流程,再逐步工程化、模块化,最后叠加 AI 能力。
七、关键实践要点
7.1 v3 SDK 的 API 变化
Milvus v3 SDK 相比 v2 有一些重要变化:
- 连接方式:从显式的
client.connect()变为自动连接,checkHealth()负责验证。 - 搜索 API:v2 使用
vector参数传入单个向量数组,v3 改为data: [vectorArray]数组格式,支持批量搜索。 - 返回结果:v3 返回
searchResult.results,每个结果的score表示相似度分数。
Demo 中 query.mjs 和 rag.mjs 分别使用了新旧两种 API 风格(query.mjs 用 vector: 而 index.mjs/rag.mjs 用 data:),这实际展示了版本的演进——实际开发中应统一使用 v3 的 data 格式。
7.2 Embedding 维度的选择
向量维度直接影响存储成本和搜索精度。1024 维是 Demo 中使用的维度,在精度和性能间取得了平衡。维度过低(如 128 维)可能丢失语义细节,维度过高(如 3072 维)则增加存储和计算开销。选择维度时应与 Embedding 模型的输出维度一致。
7.3 索引与加载
createIndex 之后必须调用 loadCollection,数据才会被加载到内存中供搜索使用。这是一个容易被忽略的步骤——没有 load,搜索会直接失败。
7.4 Collection 的幂等性
Demo 使用 hasCollection 检查 Collection 是否存在,避免重复创建导致报错。在生产环境中,这通常意味着将 Schema 定义与数据写入解耦:先手动或通过 migration 创建 Collection,应用代码只负责写入和查询。
八、总结与展望
本文通过一个 AI 日记助手的实际 Demo,完整展示了从零搭建向量搜索 + RAG 应用的全过程。核心要点回顾:
- Milvus 提供高性能的向量存储与搜索能力,Zilliz Cloud 免去了运维负担。
- LangChain 的 OpenAI 集成统一了 Embedding 和 Chat Model 的调用接口,兼容任何 OpenAI 格式的 API。
- 自定义 Schema 赋予数据更强的结构化表达能力,Array 类型等高级字段让元数据管理更灵活。
- RAG 模式是当前 AI 应用最主流且有效的范式——检索保证事实准确性,生成保证回答的自然度。
- Prompt 设计是 RAG 效果的决定性因素之一,角色设定、人称策略、行为边界都值得精心打磨。
可以进一步探索的方向
- 混合搜索(Hybrid Search):结合向量相似度 + 关键词匹配(BM25)+ 元数据过滤,实现更精准的召回。
- Partition(分区):按年份或月份对日记分区,缩小搜索范围,提升大规模数据下的性能。
- 流式输出:在 RAG 回答生成中使用 Streaming,提升用户体感响应速度。
- 多模态扩展:Milvus 不仅支持文本向量,也支持图片、音频等多模态 Embedding,可以构建"记住那天的照片"的跨模态检索。
向量数据库正在成为 AI 应用基础设施的关键一环。正如关系型数据库是 Web 2.0 的标配,向量数据库正在成为 AI Native 应用的标配。现在开始动手实践,正是最好的时机。