🔥 从 MySQL 到 Milvus:用 AI 日记本项目搞懂向量数据库和 RAG

🔥 从 MySQL 到 Milvus:用 AI 日记本项目搞懂向量数据库和 RAG

摘要 :你还在用 WHERE content LIKE '%关键词%' 搜索文本?太落后了!本文用一个「AI 日记本」项目,带你从零搞懂向量数据库 Milvus 和 RAG,让你的 AI 应用真正拥有"记忆"。


📌 前言

上周有个需求:用户输入一句话,系统要从几千条日记里找到语义最相关的几条。

我第一反应是 MySQL 全文搜索,但很快发现不对:

用户搜索 日记原文 MySQL 结果
"心情好的日子" "今天阳光明媚,去公园散步了" ❌ 搜不到
"和朋友出去玩" "周末和哥们儿去爬山了" ❌ 搜不到
"工作压力大" "今天deadline,疯狂加班" ❌ 搜不到

关键词完全对不上,但语义上高度相关!

这就是传统数据库的痛点 ------ 它只能做字面匹配 ,理解不了语义

直到我接触了 Milvus 向量数据库 + RAG,才发现:原来 AI 应用的"记忆"可以这样做。


🎯 本文适合谁

  • 有 Node.js 基础,想了解向量数据库的前端/全栈开发者
  • 听过 RAG 但不知道怎么落地的同学
  • 想给自己的 AI 应用加"长期记忆"的开发者

📚 先搞懂:什么是向量数据库?

用图书馆类比

想象你去图书馆找书:

方式 MySQL Milvus
怎么找 按书名、ISBN 查(精确匹配) 告诉管理员"我想看一本关于孤独的科幻小说"(语义匹配)
底层逻辑 WHERE title = '三体' 把你的描述和每本书的"语义指纹"做相似度计算
擅长 结构化数据的精确查询 非结构化数据的模糊语义检索

核心概念:Embedding(向量化)

向量数据库的灵魂是 Embedding ------ 把任意文本变成一组数字(向量):

arduino 复制代码
"今天心情很好"  →  [0.12, -0.34, 0.56, ..., 0.78]  (1024 个浮点数)
"开心的一天"    →  [0.11, -0.33, 0.55, ..., 0.77]  (语义相近,数字也接近)
"项目上线了"    →  [0.89, 0.12, -0.45, ..., 0.23]  (语义不同,数字差距大)

语义相近的文本,向量在空间中的距离也近 ------ 这就是向量搜索的原理。

💡 为什么需要 Embedding? 因为计算机不懂中文,但它懂数字。Embedding 就是把人类语言翻译成计算机能理解的"语义坐标"。


🛠️ 项目实战:AI 日记本

📦 完整源码:Gitee - milvus-demo/demo2/src

学习路线图:4 个文件的递进逻辑

我们用 4 个文件,循序渐进地学习:

erlang 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│  第 1 步:main.mjs --- 先用"假数据"跑通最小闭环                         │
│  目的:理解 Milvus 的核心操作:建表 → 插数据 → 搜索                   │
│  特点:手写 4 维向量,快速验证流程                                     │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  第 2 步:index.mjs --- 用真实数据 + Embedding 模型写入                 │
│  目的:学会用 Embedding 模型自动生成向量                              │
│  特点:从"假数据"升级到"真实日记数据"                                 │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  第 3 步:query.mjs --- 用自然语言搜索日记                              │
│  目的:学会用自然语言检索向量数据                                     │
│  特点:搜索"户外活动"能找到"爬山"和"公园散步"                         │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│  第 4 步:rag.mjs --- 检索 + LLM = AI 日记助手                        │
│  目的:把检索结果喂给 LLM,生成智能回答                               │
│  特点:完整 RAG 流程,AI 真正"读懂"了你的日记                         │
└─────────────────────────────────────────────────────────────────────┘

端到端数据流:从文本到回答

在开始写代码之前,先看懂整个数据流:

arduino 复制代码
┌──────────────────────────────────────────────────────────────────────┐
│                        数据写入阶段(index.mjs)                      │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   日记文本:"今天天气很好,去公园散步了"                                 │
│                    │                                                 │
│                    ▼                                                 │
│   ┌─────────────────────────────┐                                   │
│   │    Embedding 模型           │  文本 → 向量                       │
│   │  text-embedding-3-small     │                                   │
│   └──────────────┬──────────────┘                                   │
│                  ▼                                                   │
│   向量:[0.12, -0.34, 0.56, ..., 0.78](1536 维)                    │
│                  │                                                   │
│                  ▼                                                   │
│   ┌─────────────────────────────┐                                   │
│   │       Milvus 向量数据库      │  存储向量 + 元数据                  │
│   │  ai_dairy 集合              │                                   │
│   └─────────────────────────────┘                                   │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────┐
│                        检索 + 生成阶段(rag.mjs)                     │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│   用户提问:"我最近做了什么让我感到快乐的事情?"                        │
│                    │                                                 │
│                    ▼                                                 │
│   ┌─────────────────────────────┐                                   │
│   │    Embedding 模型           │  把问题也转成向量                   │
│   └──────────────┬──────────────┘                                   │
│                  ▼                                                   │
│   问题向量:[0.15, -0.32, 0.58, ..., 0.76]                           │
│                  │                                                   │
│                  ▼                                                   │
│   ┌─────────────────────────────┐                                   │
│   │       Milvus 检索           │  找到最相似的 2 条日记              │
│   │  COSINE 相似度搜索          │                                   │
│   └──────────────┬──────────────┘                                   │
│                  ▼                                                   │
│   检索结果:                                                           │
│   - 日记1:"今天天气很好,去公园散步了"(相似度 0.89)                  │
│   - 日记2:"周末和朋友去爬山"(相似度 0.85)                          │
│                  │                                                   │
│                  ▼                                                   │
│   ┌─────────────────────────────┐                                   │
│   │    拼接 Prompt              │  日记内容 + 问题 → 完整提示词       │
│   └──────────────┬──────────────┘                                   │
│                  ▼                                                   │
│   ┌─────────────────────────────┐                                   │
│   │        LLM 生成             │  AI 基于日记内容生成回答            │
│   └──────────────┬──────────────┘                                   │
│                  ▼                                                   │
│   "从你的日记来看,最近让你快乐的事情有:                              │
│    1. 去公园散步,感受春天的美好                                       │
│    2. 和朋友爬山,享受户外时光"                                       │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

💡 为什么需要这个流程? 因为 LLM 自己没有你的日记数据,它会"幻觉"。RAG 让 AI 先查资料再回答,保证回答有据可依。


1️⃣ main.mjs --- Milvus 入门:先跑通最小闭环

为什么要从这里开始? 因为在用真实数据之前,先用"假数据"验证流程是最安全的。就像学做菜先用便宜食材练手,别一上来就用和牛。

连接 Zilliz Cloud
javascript 复制代码
import { MilvusClient, IndexType, MetricType } from "@zilliz/milvus2-sdk-node";

const client = new MilvusClient({
    address: process.env.MILVUS_ADDRESS,  // Zilliz Cloud 地址
    token: process.env.MILVUS_TOKEN        // API Key
});

// 检查连接
const checkHealth = await client.checkHealth();
console.log(checkHealth.isHealthy ? '✅ 连接成功' : '❌ 连接失败');

💡 Zilliz Cloud 是 Milvus 的全托管云服务,不用自己部署,注册就能用。

创建集合(类似 MySQL 建表)
javascript 复制代码
await client.createCollection({
    collection_name: 'test',
    dimension: 4,      // 向量维度,这里用 4 维方便演示
    auto_id: true       // 自动生成 ID
});
创建索引 ------ 为什么需要?

没有索引时,搜索要逐一计算所有向量的相似度,时间复杂度 O(n)。数据量大了就慢得离谱。

IVF_FLAT 索引的原理类似图书馆分区:

scss 复制代码
没有索引:在整栋楼里一本本翻          → O(n)
有索引:  先去"文学区",再找"科幻"    → 近似 O(1)
javascript 复制代码
await client.createIndex({
    collection_name: 'test',
    field_name: 'vector',
    index_type: IndexType.AUTOINDEX,
    metric_type: MetricType.COSINE   // 用余弦相似度
});
插入和搜索
javascript 复制代码
// 插入数据(手动指定的 4 维向量)
const data = [
    { vector: [0.1, 0.2, 0.3, 0.4], content: '这是第一条数据' },
    { vector: [0.5, 0.6, 0.7, 0.8], content: '这是第二条数据' }
];
await client.insert({ collection_name: 'test', data });

// 搜索:找和 [0.5, 0.5, 0.6, 0.8] 最相似的 2 条
const searchRes = await client.search({
    collection_name: 'test',
    data: [[0.5, 0.5, 0.6, 0.8]],
    limit: 2,
    output_fields: ['content']
});

console.log('搜索结果:', searchRes.results);

到这里,你已经理解了 Milvus 的核心三板斧:建集合 → 插数据 → 搜索。


2️⃣ index.mjs --- 用 Embedding 模型写入真实数据

为什么要升级到这一步? 因为手写向量不现实。实际项目中,你需要用 Embedding 模型自动把文本转成向量。

定义集合结构
javascript 复制代码
import { DataType } from "@zilliz/milvus2-sdk-node";

await client.createCollection({
    collection_name: 'ai_dairy',
    fields: [
        { name: 'id', data_type: DataType.VarChar, max_length: 50, is_primary_key: true },
        { name: 'vector', data_type: DataType.FloatVector, dim: 1024 },  // 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 }
    ]
});

跟 MySQL 建表几乎一样,只是多了一个 FloatVector 类型的 vector 字段。

Embedding:文本 → 向量
javascript 复制代码
import { OpenAIEmbeddings } from '@langchain/openai';

const embedding = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.EMBEDDINGS_MODEL_NAME   // 比如 text-embedding-3-small
});

const getEmbedding = async (text) => {
    return await embedding.embedQuery(text);    // 一段文本 → 1024 维向量
};

💡 为什么要用 Embedding 模型? 因为它能把"语义"编码成数字。手动写向量无法捕捉语义信息。

准备日记数据 + 写入
javascript 复制代码
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: ['美食', '家庭'] }
];

// 给每条日记加上向量
const diaryData = await Promise.all(
    diaryContents.map(async (diary) => ({
        ...diary,
        vector: await getEmbedding(diary.content)   // 自动生成向量
    }))
);

// 批量插入
const insertResult = await client.insert({
    collection_name: 'ai_dairy',
    data: diaryData
});
console.log('✅', insertResult.insert_cnt, '条记录成功插入。');

// 重要:插入后重新加载集合!
await client.loadCollection({ collection_name: 'ai_dairy' });

关键流程

yaml 复制代码
文本日记 → Embedding 模型 → 1024 维向量 → 存入 Milvus

3️⃣ query.mjs --- 用自然语言搜索日记

为什么要单独讲搜索? 因为写入和搜索是两个独立的操作。写入是一次性的,搜索是实时的。而且搜索涉及"把用户问题也转成向量"这个关键步骤。

javascript 复制代码
// 用户说了一句话
const query = '我想看看关于户外活动的日记';

// 1. 把查询文本也转成向量(这一步很关键!)
const queryVector = await getEmbedding(query);

// 2. 去 Milvus 里找最相似的 2 条
const searchResult = await client.search({
    collection_name: 'ai_dairy',
    vector: queryVector,
    limit: 2,
    metric_type: MetricType.COSINE,
    output_fields: ['id', 'content', 'date', 'mood', 'tags']
});

// 3. 打印结果
console.log('🔍 搜索结果:\n');
searchResult.results.forEach((item, index) => {
    console.log(`${index + 1}. [Score: ${item.score.toFixed(4)}]`);
    console.log(`   内容:${item.content}`);
    console.log(`   心情:${item.mood}`);
    console.log(`   日期:${item.date}\n`);
});

输出大概是:

css 复制代码
🔍 搜索结果:

1. [Score: 0.8923]
   内容:周末和朋友去爬山,天气很好,心情也很放松。
   心情:relaxed
   日期:2026-01-12

2. [Score: 0.8156]
   内容:今天天气很好,去公园散步了,心情愉快。
   心情:happy
   日期:2026-01-10

用户说"户外活动",系统找到了"爬山"和"公园散步" ------ 没有任何关键词匹配,纯靠语义理解!


4️⃣ rag.mjs --- 检索 + LLM = AI 日记助手

为什么要加 RAG? 因为光检索还不够。用户问"我最近快乐吗?",你不能只返回几条日记,而是要让 AI 分析这些日记,给出一个有温度的回答。

什么是 RAG?

RAG = R etrieval(检索)+ A ugmented(增强)+ Generation(生成)

用图书馆类比:

markdown 复制代码
1. 检索(Retrieval)  :去图书馆找几本相关的书
2. 增强(Augmented)  :把书翻到相关页面,放在桌上
3. 生成(Generation) :让 AI 看着这些书来回答问题

没有 RAG :AI 只能靠自己的"记忆"回答,可能胡说八道(幻觉)。 有了 RAG:AI 先查资料再回答,有理有据。

模块一:检索(Retrieval)
javascript 复制代码
async function retrieveRelevantDiaries(question, k = 2) {
    const queryVector = await getEmbedding(question);
    const searchResult = await client.search({
        collection_name: 'ai_dairy',
        limit: k,
        metric_type: MetricType.COSINE,
        vector: queryVector,
        output_fields: ['id', 'content', 'date', 'mood', 'tags']
    });
    return searchResult.results;
}

这一步就是前面 query.mjs 做的事 ------ 找到最相关的 k 条日记。

模块二:增强(Augmented)
javascript 复制代码
// 把检索到的日记拼成一段"上下文"
function buildContext(retrievedDiaries) {
    return retrievedDiaries.map((diary, i) => `
[日记 ${i + 1}]
日期:${diary.date}
心情:${diary.mood}
标签:${diary.tags?.join(', ')}
内容:${diary.content}
`).join('\n----\n');
}

为什么要拼接上下文? 因为 LLM 需要"参考资料"才能准确回答。就像你回答问题时,老师会给你一些参考材料。

模块三:生成(Generation)
javascript 复制代码
async function generateAnswer(question, context) {
    const prompt = `你是一个温暖贴心的AI日记助手。
基于用户的日记内容回答问题,用亲切自然的语言。

请根据以下日记内容回答问题:

${context}

用户问题:${question}

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

AI 助手回答:`;

    const response = await model.invoke(prompt);
    return response.content;
}
完整调用流程
javascript 复制代码
// RAG 完整流程
async function aiDiaryAssistant(question) {
    console.log(`\n📝 用户提问:${question}\n`);

    // 1. 检索
    console.log('🔍 正在检索相关日记...');
    const retrievedDiaries = await retrieveRelevantDiaries(question, k = 3);

    // 2. 构建上下文
    const context = buildContext(retrievedDiaries);

    // 3. 生成回答
    console.log('🤖 AI 正在生成回答...\n');
    const answer = await generateAnswer(question, context);

    console.log('💡 AI 回答:');
    console.log(answer);

    return answer;
}

// 测试
await aiDiaryAssistant('我最近做了什么让我感到快乐的事情?');

输出示例:

markdown 复制代码
📝 用户提问:我最近做了什么让我感到快乐的事情?

🔍 正在检索相关日记...
🤖 AI 正在生成回答...

💡 AI 回答:
从你的日记来看,最近让你感到快乐的事情有:

1. **去公园散步**(1月10日)------ 你提到"天气很好,心情愉快",看得出来你很享受亲近自然的时光。

2. **和朋友爬山**(1月12日)------ 周末和朋友一起户外活动,你说"心情也很放松",友情和运动都让你开心。

3. **尝试新菜谱**(1月13日)------ 你为自己做了一顿丰盛的晚餐,这种自我照顾的小确幸也很珍贵呢 😊

看得出来你是一个热爱生活、懂得享受当下的人。继续保持这份对生活的热情吧!

💡 关键技术点总结

概念 一句话解释 在项目中的作用
Milvus 专门存和查向量的数据库 存储日记的向量表示
Embedding 文本 → 数字数组 把日记和问题转成向量
COSINE 相似度 衡量两个向量方向的相似程度 找到语义最相关的日记
IVF_FLAT 索引 聚簇索引,加速搜索 让搜索更快
RAG 检索 + LLM 生成 让 AI 基于日记内容回答
Zilliz Cloud Milvus 的全托管云服务 免去部署烦恼

🐛 踩坑记录

坑 1:查到 0 条数据

现象:insert 明明成功了,search 返回空数组。

原因loadCollectioninsert 之前调用了,新插入的数据没有被加载到内存。

解决 :插入数据后,重新调用 loadCollection

javascript 复制代码
await client.insert({ collection_name: 'ai_dairy', data: diaryData });
await client.loadCollection({ collection_name: 'ai_dairy' });  // 重新加载!

预防:养成习惯,每次 insert 后都 load 一次。


坑 2:output_fields 字段名拼错

现象 :返回的字段值全是 undefined

原因output_fields 里写的是 'data',实际字段名是 'date'

解决:仔细检查集合的字段定义,别拼错。

预防:复制字段名,不要手打。


坑 3:向量维度不匹配

现象 :创建集合时 dim: 1024,但 Embedding 模型生成的向量维度不是 1024。

解决 :确保 dim 参数和 Embedding 模型输出的维度一致。常用模型的维度:

模型 维度
text-embedding-3-small 1536
text-embedding-3-large 3072
text-embedding-ada-002 1536

预防:在创建集合前,先调用一次 Embedding API 确认输出维度。


🔗 参考资料


💬 交流讨论

你在项目中用过向量数据库吗?有没有遇到什么坑?欢迎评论区交流!

觉得有用?点个赞 👍 收藏 ⭐ 关注 👆,后续会更新更多 AI 实战内容!

相关推荐
网络安全零基础教程1 小时前
网络安全春招面试避坑与复盘指南
linux·网络·安全·web安全·面试·职场和发展
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(三十九):前端上传组件实现
javascript·vue.js·ai编程
众人皆醒我独醉1 小时前
AI 怎么"理解"一个词的意思?—— Embedding 是把整个世界压缩成一组数字
人工智能·面试
dota李天王1 小时前
简单的 socket 实现
前端·javascript·websocket
Oo9201 小时前
从零搭建 AI 日记本:Milvus 向量数据库 + RAG 实战
人工智能
夜郎king1 小时前
解决AI图文解析偏差:CodeBuddy多模型交叉校验+腾讯地图Skill经纬度定位实战
大数据·人工智能
智慧物业老杨1 小时前
物业费公共收益维修资金三类资金分账的合规管控
大数据·人工智能·物联网
kingcjh971 小时前
具身智能数据处理的工程化实践
人工智能·数据挖掘
dadaobusi1 小时前
Sniper
人工智能