从零搭建 Milvus 向量知识库:Node.js 实现日记 RAG 检索全流程实战

向量数据库是 RAG(检索增强生成)架构的核心基石,Milvus 作为业界主流的开源向量数据库,搭配 Zilliz Cloud 云端托管服务,可以快速搭建生产级向量检索系统。本文将基于 Node.js 生态,从零实现一个日记知识库的向量存储、检索与智能问答全流程,完整覆盖环境搭建、集合设计、索引原理、文本向量化、批量入库、相似度检索、大模型问答闭环。

一、技术选型与前置准备

1.1 核心技术栈

  • 向量数据库:Milvus(Zilliz Cloud 云端托管),免去本地部署运维成本
  • 开发语言:Node.js + ES Module
  • 向量化模型:OpenAI 兼容 Embedding 接口(支持通义千问、本地模型等替换)
  • 大语言模型:ChatOpenAI 兼容接口,负责基于检索内容生成自然语言回答
  • 开发框架:LangChain.js 统一封装向量化与大模型调用能力
  • 环境管理:dotenv 管理密钥与配置

1.2 依赖安装

项目核心依赖为 Milvus 官方 SDK、向量化工具与大模型 SDK:

bash

sql 复制代码
pnpm add @zilliz/milvus2-sdk-node dotenv @langchain/openai

踩坑提示:Windows 环境下若项目位于 OneDrive 同步目录,会出现 PNPM EPERM 文件锁定报错,建议将项目迁移至纯本地路径,或暂停 OneDrive 同步后执行安装。

1.3 环境变量配置

在项目根目录创建 .env 文件,填入 Zilliz 集群、向量化与大模型接口配置:

env

ini 复制代码
# Zilliz Cloud 连接信息
MILVUS_ADDRESS=https://xxx.ali-cn-hangzhou.vectordb.zillizcloud.com:19530
MILVUS_TOKEN=你的API密钥

# Embedding 向量化配置
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EMBEDDINGS_MODEL_NAME=text-embedding-v3

# 大模型问答配置
MODEL_NAME=qwen-plus

二、Milvus 核心概念解析

在写代码之前,先理清向量数据库的核心概念,对应关系如下:

表格

MySQL 概念 Milvus 概念 说明
数据库 Database 项目 Project 资源隔离层级
数据表 Table 集合 Collection 存储向量与元数据的基本单元
列 Column 字段 Field 包括向量字段与标量字段
索引 Index 向量索引 加速向量相似度检索,避免全量暴力计算
行 Row 实体 Entity 一条完整的向量 + 元数据记录

2.1 向量索引:检索速度的核心

没有索引时,每次查询都要遍历全库向量逐一计算相似度,时间复杂度 O (n),数据量增大后性能极差。

本项目选用 IVF_FLAT(倒排文件索引)

  • 原理:提前将全量向量聚类分成 N 个桶,检索时只匹配最接近的少数桶,大幅缩小计算范围
  • 优势:参数简单、精度可控,百万级数据内综合性能均衡
  • 适用场景:知识库、日记检索等中小规模向量场景

2.2 相似度度量:COSINE 余弦相似度

文本向量化场景统一使用余弦相似度:

  • 计算向量方向的夹角,不受向量长度影响
  • 分值区间 -1, 1,越接近 1 代表语义相似度越高
  • 是 Embedding 文本检索的行业标准度量方式

三、完整代码实现:日记向量知识库

3.1 导入依赖与初始化客户端

javascript

运行

arduino 复制代码
import "dotenv/config";
import {
  MilvusClient,
  MetricType,
  IndexType,
  DataType,
} from "@zilliz/milvus2-sdk-node";
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai";

// 全局常量配置
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const COLLECTION_NAME = "ai_diary";
const VECTOR_DIM = 1024;

// 初始化向量化工具
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDINGS_MODEL_NAME,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
  dimensions: VECTOR_DIM,
});

// 初始化大语言模型
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,
  },
});

// 初始化 Milvus 客户端
const client = new MilvusClient({
  address: ADDRESS,
  token: TOKEN,
  ssl: true, // Zilliz Cloud 强制开启 SSL
});

// 封装文本转向量函数
const getEmbedding = async (text) => {
  const result = await embeddings.embedQuery(text);
  return result;
};

代码解析

  • MilvusClient 是 SDK 核心入口,负责所有数据库操作
  • ssl: true 为云端必填项,本地部署可省略
  • temperature: 0.1 控制大模型输出稳定性,问答场景推荐偏低的温度,减少幻觉
  • getEmbedding 统一封装向量化逻辑,后续替换模型只需修改该函数

3.2 集合 Schema 设计

日记场景需要存储向量 + 业务元数据,字段设计如下:

javascript

运行

php 复制代码
async function createCollection() {
  await client.createCollection({
    collection_name: COLLECTION_NAME,
    fields: [
      {
        name: "id",
        data_type: DataType.VarChar,
        max_length: 50,
        is_primary_key: true,
      },
      {
        name: "vector",
        data_type: DataType.FloatVector,
        dim: VECTOR_DIM,
      },
      {
        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,
      },
    ],
  });
  console.log("集合创建成功");
}

设计要点

  • 主键使用自定义字符串 ID,便于业务侧关联原始日记
  • vector 字段维度必须与 Embedding 模型输出维度严格一致,否则插入报错
  • tags 使用数组类型,支持多标签存储与过滤
  • 对比 MySQL,Milvus 支持动态字段,未预先定义的字段也可插入,灵活性更高

3.3 创建向量索引与加载集合

javascript

运行

php 复制代码
async function createIndexAndLoad() {
  // 创建向量索引
  await client.createIndex({
    collection_name: COLLECTION_NAME,
    field_name: "vector",
    index_type: IndexType.IVF_FLAT,
    metric_type: MetricType.COSINE,
    params: { nlist: 128 }, // 聚类分桶数量
  });
  console.log("索引创建成功");

  // 加载集合到内存(检索前必须执行)
  await client.loadCollection({
    collection_name: COLLECTION_NAME,
  });
  console.log("集合加载完成");
}

关键说明

  • nlist 是 IVF_FLAT 的核心参数,百万级数据推荐 128,数据量增大可上调
  • loadCollection 是 Milvus 的必要步骤,未加载的集合无法执行检索

3.4 批量向量化与数据入库

javascript

运行

less 复制代码
async function insertDiaries() {
  // 原始日记数据
  const diaryContents = [    {      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: ["美食", "家庭"],
    },
  ];

  console.log("正在批量生成向量...");
  // 并发调用向量化接口,提升处理速度
  const diaryData = await Promise.all(
    diaryContents.map(async (diary) => ({
      ...diary,
      vector: await getEmbedding(diary.content),
    }))
  );

  // 批量插入 Milvus
  const insertResult = await client.insert({
    collection_name: COLLECTION_NAME,
    data: diaryData,
  });
  console.log(`${insertResult.insert_cnt} 条记录成功插入`);
}

性能优化点

  • 使用 Promise.all 并发生成向量,相比串行 await 速度提升数倍
  • 批量插入比单条循环插入性能更优,生产环境建议分批批量写入
  • 数据量过大时需控制并发数,避免触发 Embedding 接口限流

3.5 向量相似度检索(基础测试版)

单文件快速验证检索能力,可独立运行调试:

javascript

运行

javascript 复制代码
async function searchTest() {
  try {
    console.log("Connection to Milvus...");
    await client.connectPromise; // 等待连接握手完成
    console.log("Connected");

    const query = "我想看看关于户外活动的日记";
    console.log(`QUERY: ${query}`);

    // 1. 问题文本生成向量
    const queryVector = await getEmbedding(query);

    // 2. 执行向量相似度检索
    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      data: [queryVector], // 查询向量,二维数组格式
      limit: 2,
      metric_type: MetricType.COSINE,
      output_fields: ["id", "content", "date", "mood", "tags"],
    });

    // 3. 格式化打印结果
    console.log(`Found ${searchResult.results[0].length} results`);
    searchResult.results[0].forEach((item, index) => {
      console.log(`${index + 1}. [Score: ${item.score.toFixed(4)}]`);
      console.log(`
      ID: ${item.id}
      Date: ${item.date}
      Mood: ${item.mood}
      Tags: ${item.tags?.join(", ")}
      Content: ${item.content}  
      `);
    });
  } catch (err) {
    console.error("检索失败:", err.message);
  }
}

检索参数说明

  • data:传入查询向量数组,二维格式,支持批量多向量查询
  • limit:返回最相似的 Top K 条结果
  • output_fields:指定返回的标量字段,不指定只返回 ID 和相似度分数
  • item.score:余弦相似度分值,越接近 1 语义匹配度越高
  • connectPromise:等待 Milvus 客户端完成 TCP 握手,避免未连接就发起请求报错

四、模块化 RAG 问答系统完整实现

RAG 的核心思想是检索 + 生成两阶段架构:先从向量库召回相关知识,再把知识作为上下文传给大模型,让 AI 基于私有数据回答问题。本项目将检索与生成拆分为独立模块,职责清晰、便于维护。

4.1 检索模块封装:retrieveRelevantDiaries

独立封装向量检索逻辑,上层业务无需关心 Milvus 底层细节:

javascript

运行

php 复制代码
async function retrieveRelevantDiaries(question, k = 2) {
  try {
    const queryVector = await getEmbedding(question);
    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      data: [queryVector],
      limit: k,
      metric_type: MetricType.COSINE,
      output_fields: ["id", "content", "date", "mood", "tags"],
    });
    // 返回匹配的日记实体数组
    return searchResult.results[0];
  } catch (err) {
    console.log("检索日记时出错", err.message);
    return [];
  }
}

设计优势

  • 单一职责:只负责向量召回,不处理业务逻辑
  • 异常兜底:检索失败返回空数组,上层业务可优雅降级
  • 可复用:问答、搜索、推荐等多个场景都可调用该函数

4.2 问答核心:answerDiaryQuestion 完整实现

整合检索、上下文拼接、Prompt 构建、大模型调用全流程:

javascript

运行

javascript 复制代码
async function answerDiaryQuestion(question, k = 2) {
  try {
    // 打印分隔线,便于调试日志区分
    console.log("=".repeat(80));
    console.log(`问题:${question}`);
    console.log("=".repeat(80));

    // 阶段一:向量检索召回相关日记
    console.log("检索相关日记");
    const retrievedDiaries = await retrieveRelevantDiaries(question, k);
    if (retrievedDiaries.length === 0) {
      console.log("未找到相关日记");
      return;
    }

    // 打印检索结果与相似度,便于调试召回效果
    retrievedDiaries.forEach((diary, i) => {
      console.log(`日记${i + 1} 相似度: ${diary.score.toFixed(4)}
      内容:${diary.content}
      `);
    });

    // 阶段二:拼接格式化上下文
    const context = retrievedDiaries
      .map((diary, i) => `
        [日记 ${i + 1}]
        日期:${diary.date}
        心情: ${diary.mood}
        标签: ${diary.tags?.join(", ")}
        内容:${diary.content}
      `)
      .join("\n\n----\n\n");

    // 阶段三:构建系统提示词 + 上下文 + 用户问题
    const prompt = `你是一个温暖贴心的AI日记助手。基于用户的日记内容回答问题,
    用亲切自然的语言。请根据以下日记内容回答问题:
    ${context}

    用户问题: ${question}

    回答要求:
    1. 如果日记中有相关信息,请结合日记内容给出详细、温暖的回答。
    2. 可以总结多篇日记的内容,找出共同点或趋势。
    3. 如果日记中没有相关信息,请温和告知用户。
    4. 用第一人称"你"来称呼日记的作者。
    5. 回答要有同理心,让用户感到被理解和关心。

    AI 助手的回答:
    `;

    // 阶段四:调用大模型生成回答
    console.log("[AI 回答]");
    const response = await model.invoke(prompt);
    console.log(response.content);
  } catch (err) {
    console.log(err.message);
  }
}

核心设计解析

  1. 模块化拆分:严格遵循 RAG 两阶段,检索逻辑独立封装,问答函数只负责流程编排

  2. Prompt 工程

    • 设定角色人设:温暖贴心的日记助手,统一回答风格
    • 明确回答规则:5 条约束减少幻觉,强制基于日记内容回答
    • 人称规范:用 "你" 称呼作者,增强对话代入感
  3. 调试友好:打印相似度分值与召回原文,便于排查「回答不准」是召回问题还是生成问题

  4. 异常兜底:检索为空时友好提示,不会触发大模型空上下文报错

4.3 主函数入口与流程串联

javascript

运行

javascript 复制代码
async function main() {
  try {
    console.log("连接到Milvus...");
    await client.connectPromise; // 等待连接就绪
    console.log("已连接");

    // 执行日记智能问答
    await answerDiaryQuestion("我最近做了什么让我感到快乐的事情?", 2);

    // 关闭连接
    await client.close();
  } catch (err) {
    console.error("程序执行异常:", err);
  }
}

main().catch(console.error);

五、常见踩坑与解决方案

  1. 集合重复创建报错 :每次运行前通过 hasCollection 判断是否已存在,避免重复执行 DDL
  2. 检索无结果 / 报错 :必须执行 loadCollection 加载集合到内存,否则无法查询
  3. 向量维度不匹配:建表 dim 与 Embedding 输出维度必须严格一致
  4. 插入后检索不到 :新插入数据需要落盘,可手动执行 flush 强制持久化
  5. Windows 安装依赖 EPERM:项目移出 OneDrive 同步目录,或暂停云盘同步
  6. search 参数报错 :SDK 标准参数为 data(二维数组),而非 vector,需注意格式
  7. 大模型回答幻觉:降低 temperature、在 Prompt 中强约束「仅基于日记内容回答」、增加召回条数可有效缓解

六、总结

本文完整实现了基于 Milvus 的日记向量知识库与 RAG 智能问答系统,覆盖了从环境搭建、Schema 设计、索引原理到批量入库、相似度检索、大模型问答的全链路。通过模块化拆分,检索层与生成层完全解耦,后续替换向量模型、大模型或向量数据库都无需大幅改动业务代码。

向量数据库 + RAG 是当前落地私有知识库最成熟的方案,掌握这套基础架构后,可以快速拓展到文档问答、客服机器人、语义搜索、个人记忆助手等更多场景。

相关推荐
张元清2 小时前
React usePrevious Hook:追踪上一次的 State 和 Props(2026)
javascript·react.js
Cobyte2 小时前
手写 Rollup 插件实现解析 Vue 文件
前端·javascript·vue.js
安冬的码畜日常2 小时前
【工欲善其事】深入理解 Node.js 带并发上限的异步任务批量执行逻辑
javascript·设计模式·node.js·ai编程·异步编程·并发执行
meilindehuzi_a4 小时前
Vue3组件样式隔离与组件通信实战:从 scoped、props 校验到记事本应用
前端·javascript·vue.js
To_OC11 小时前
我被 useState 坑了两次之后,终于把它的脾气摸透了
前端·javascript·react.js
Luu.ྀ12 小时前
React详细笔记
javascript·笔记·react.js
kyriewen16 小时前
我review了一份Vibe Coding写的前端代码——能跑,但5个地方迟早要命
前端·javascript·ai编程
Hilaku18 小时前
为什么在 2026 年,MPA(多页面应用)正在悄悄复辟?
前端·javascript·程序员
悟空瞎说19 小时前
iOS 高效绘图
javascript