从零搭建 AI 日记助手:用 Milvus 向量数据库实现语义搜索

从零搭建 AI 日记助手:用 Milvus 向量数据库实现语义搜索

前言:当 AI 拥有了记忆

近两年 Agentic AI(智能体 AI)的爆发,让应用不再只是被动地回答问题,而是能够主动规划、调用工具、甚至拥有长期记忆。而支撑这种"记忆"的关键基础设施,就是向量数据库。今天我们就借助开源向量数据库 Milvus,从零实现一个会"读懂"日记内容的语义搜索引擎------你说"想看看关于户外活动的日记",它就能精准找到爬山、散步等相关的记录。


一、为什么我们需要向量数据库?

传统关系型数据库(如 MySQL、PostgreSQL)擅长精确匹配和结构化查询:

sql 复制代码
SELECT * FROM diaries WHERE tags LIKE '%户外%';

但自然语言是模糊的------"户外活动"可能对应"爬山""公园散步""骑自行车"等各种说法,光靠关键词匹配远远不够。而向量数据库将文本转化为高维空间中的向量,通过计算向量之间的余弦相似度,可以找到语义最相近的内容,完全不需要死板的关键词。

Milvus 就是这样一款为 AI 场景设计的开源向量数据库,支持百亿级向量检索,已经被大量 AI Agent 产品使用。结合大模型的 Embedding 接口,我们可以轻松赋予应用真正的语义理解能力。


二、项目结构概览

我们将使用 Node.js 环境,结合两个核心库:

  • @zilliz/milvus2-sdk-node:Milvus 官方 Node SDK
  • @langchain/openai:调用 OpenAI 兼容的 Embedding 模型

最终实现的功能:

  1. 设计日记的存储结构(id、向量、内容、日期、心情、标签)
  2. 将日记文本转为向量写入 Milvus
  3. 输入查询语句,返回语义最匹配的日记条目

三、环境准备与嵌入模型配置

首先安装依赖:

bash 复制代码
npm install @zilliz/milvus2-sdk-node @langchain/openai dotenv

.env 文件中配置连接信息和 API Key:

env 复制代码
MILVUS_ADDRESS=your_milvus_host:port
MILVUS_TOKEN=your_milvus_token
OPENAI_API_KEY=your_openai_api_key
OPENAI_BASE_URL=https://api.openai.com/v1   # 或任何兼容的地址
EMBEDDING_MODEL_NAME=text-embedding-3-small

我们使用 @langchain/openai 提供的 OpenAIEmbeddings 封装,方便切换模型,并可以通过 dimensions 参数指定输出向量维度(示例中使用 1024,可根据你的模型调整):

javascript 复制代码
import { OpenAIEmbeddings } from '@langchain/openai'

const embeddings = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.EMBEDDING_MODEL_NAME,
    configuration: {
        baseURL: process.env.OPENAI_BASE_URL,
    },
    dimensions: VECTOR_DIM, // 1024
})

封装一个简单的 embedding 函数,用于后续将文本转换为向量:

javascript 复制代码
async function getEmbeddings(text) {
    const result = await embeddings.embedQuery(text);
    return result;
}

四、连接 Milvus 实例

创建客户端并检查健康状态,这是所有操作的前提:

javascript 复制代码
import { MilvusClient } from '@zilliz/milvus2-sdk-node'

const client = new MilvusClient({
    address: process.env.MILVUS_ADDRESS,
    token: process.env.MILVUS_TOKEN,
})

const health = await client.checkHealth();
if (!health.isHealthy) {
    console.error('连接失败,请检查地址和 Token');
    return;
}
console.log('Milvus 连接成功!');

五、设计 Collection 的 Schema

与传统数据库中的"表"类似,Milvus 使用 Collection 组织数据。我们需要定义字段(Field)结构:

  • id:主键,字符串类型,便于区分日记
  • vector:浮点型向量字段,维度与嵌入模型输出一致
  • content:日记正文
  • date:日期
  • mood:心情标签
  • tags:数组类型的标签,可存放多个关键词
javascript 复制代码
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: 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 },
    ]
})

接着创建索引,这是实现高效向量检索的关键。我们选择 IVF_FLAT 索引,配合余弦相似度度量:

javascript 复制代码
await client.createIndex({
    collection_name: 'ai_diary',
    field_name: 'vector',
    index_type: IndexType.IVF_FLAT,
    metric_type: MetricType.COSINE,
    params: { nlist: VECTOR_DIM }
})

小贴士:nlist 是聚类单元数,一般设为向量维度的数值可以快速上手,实际场景需根据数据量调优。


六、写入日记:嵌入与插入

准备好一组示例日记,利用之前封装的 getEmbeddings 将每条 content 转化为向量,然后批量插入:

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 getEmbeddings(diary.content),
    }))
);

const insertRes = await client.insert({
    collection_name: 'ai_diary',
    data: diaryData,
});
console.log(`成功插入 ${insertRes.insert_cnt} 条数据`);

Milvus 的 insert 支持一次写入多条,非常便捷。


七、语义搜索:让机器"读懂"你想找什么

这是我们最期待的部分。用户输入查询语句 我想看看关于户外活动的日记,我们先将其向量化,然后调用 Milvus 的 search 接口,指定度量方式为余弦相似度,并返回最接近的 3 条记录。

注意在搜索前需要将 Collection 加载到内存 中(如果是已存在的 Collection):

javascript 复制代码
await client.loadCollection({ collection_name: 'ai_diary' });

const query = '我想看看关于户外活动的日记';
const queryVector = await getEmbeddings(query);

const searchResult = await client.search({
    collection_name: 'ai_diary',
    vector: queryVector,
    output_fields: ['id', 'content', 'date', 'mood', 'tags'],
    limit: 3,
    metric_type: MetricType.COSINE,
});

最后遍历结果并打印:

javascript 复制代码
searchResult.results.forEach((result) => {
    console.log('ID:', result.id);
    console.log('内容:', result.content);
    console.log('日期:', result.date);
    console.log('心情:', result.mood);
    console.log('标签:', result.tags);
    console.log('---');
});

如图

不出意外,你会看到"周末和朋友去爬山"这条日记排在前面,而纯工作或做饭的内容不会被匹配。这就是向量搜索的魅力------它理解的是语义,而非字符。


八、传统关系型 vs 向量数据库在前端眼中的分工

让我们回到开篇的对比:

  • 传统数据库(MySQL):承载文章列表、详情页、基于确定条件(如日期区间、标签筛选)的查询。后端主要做 CRUD。
  • 向量数据库(Milvus) :承载 ChatBot 的语义理解、智能搜索页面、推荐系统。后端除了增删改,核心增加了 embedding 生成retriever 检索

对于前端同学来说,虽然看不见向量数据库,但你使用的"智能搜索"或者"AI 对话记录查找"功能,背后几乎都有类似 Milvus 的组件在默默工作。


九、总结与展望

通过这个小小的 AI 日记项目,我们完成了:

  1. Milvus 实例的连接与 Collection 创建
  2. 使用 OpenAI Embeddings 将文本转化为向量
  3. 向量数据写入与索引构建
  4. 自然语言语义搜索的完整流程

这一切都不到 100 行核心代码。由此可以衍生出更多玩法:比如结合大语言模型实现"对话式日记检索",或者根据用户当前心情自动推荐过往的相似日记。

向量数据库已经不再是实验室里的概念,而是每一个准备拥抱 AI 的开发者工具箱中必备的武器。Milvus 作为开源领域的佼佼者,以高性能和丰富的生态,绝对值得你深入探索。

附录

完整代码

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

const VECTOR_DIM=1024;
const COLLECTION_NAME='ai_diary';

const TOKEN=process.env.MILVUS_TOKEN;
const ADDRESS=process.env.MILVUS_ADDRESS;

const embeddings=new OpenAIEmbeddings({
    apiKey:process.env.OPENAI_API_KEY,
    model:process.env.EMBEDDING_MODEL_NAME,
    configuration:{
        baseURL:process.env.OPENAI_BASE_URL,
    },
    dimensions:VECTOR_DIM,
})

const client=new MilvusClient({
    address:ADDRESS,
    token:TOKEN,
})
//嵌入,将文本转为向量的函数封装
async function getEmbeddings(text){
    const result=await embeddings.embedQuery(text);
    return result;
}

async function main(){
   console.log('正在连接Milvus...');
   const checkHealth=await client.checkHealth();
   if(!checkHealth.isHealthy){
    console.log('Milvus连接失败:',checkHealth);
    return;

   }

   console.log('Milvus连接成功');
   await client.loadCollection({
    collection_name:COLLECTION_NAME,
   })

   const query='我想看看关于户外活动的日记';
   const queryVector=await getEmbeddings(query);
   const searchResult=await client.search({
      collection_name:COLLECTION_NAME,
      vector:queryVector,
      output_fields:['id','content','date','mood','tags'],
      limit:3,
      metric_type:MetricType.COSINE,
   })

 searchResult.results.forEach((result)=>{
    console.log('ID:',result.id);
    console.log('内容:',result.content);
    console.log('日期:',result.date);
    console.log('心情:',result.mood);
    console.log('标签:',result.tags);
    console.log('---');
 })
 console.log('搜索完成');
 console.log('---')







 /*
   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}

    ]

   })

   await client.createIndex({
    collection_name:COLLECTION_NAME,
   field_name:'vector',//常用的查询字段
   index_type:IndexType.IVF_FLAT,
   metric_type:MetricType.COSINE,
    params:{
        nlist:VECTOR_DIM,
    }
   })
    */
   
/*
   console.log('\nInserting diary entries...');
    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('Generating embeddings...');

        const diaryData=await Promise.all(
            diaryContents.map(async (diary)=>({
                ...diary,
                vector:await getEmbeddings(diary.content),
            }))
        );
        const inserRes=await client.insert({
            collection_name:COLLECTION_NAME,
            data:diaryData,
        })
        console.log(`插入成功:${inserRes.insert_cnt}条数据`)
        */
}

main();

如果你也在学习向量数据库或构建 AI 应用,欢迎在评论区交流你的实践与困惑!

相关推荐
threelab2 小时前
Three.js UV 图像变换效果 | 三维可视化 / AI 提示词
javascript·人工智能·uv
竹林8184 小时前
用Viem替代ethers.js:从一次签名失败到完整迁移的实战记录
前端·javascript
不可能的是5 小时前
Claude Code 子 Agent 机制全解:怎么跑起来、怎么被管理、怎么互不干扰
javascript
HSunR5 小时前
dify 搭建ai作业批改流
开发语言·前端·javascript
代码不加糖6 小时前
2026 跨境电商独立站实战:从 0 到 1 搭建高转化 SaaS 商城(附源码)
开发语言·前端·javascript
用户617517157017 小时前
关于普通函数和箭头函数的this
javascript
RPGMZ8 小时前
RPGMakerMZ 地图存档点制作 标题继续游戏直接读取存档
开发语言·javascript·游戏·游戏引擎·rpgmz·rpgmakermz
有一个好名字8 小时前
Agent Loop —— 一切从那个 while 循环开始
前端·javascript·chrome
EF@蛐蛐堂8 小时前
【js】浏览器滚动条优化组件OverlayScrollbars
开发语言·javascript·ecmascript