前言你还在用关键词Like搜索痛点拉满做前端/Node后端写站内搜索时大家基本都踩过文字匹配的坑搜「Vue开发技巧」文章标题只写了「前端框架Vue实战」关键词匹配直接搜不到搜「土豆做法」数据库like模糊匹配匹配不出「酸辣土豆丝教程」海量文章数据全量模糊查询性能爆炸多关键词分词逻辑写起来巨繁琐传统文字匹配只认字面字符不懂语义。今天带你从零手写一套轻量RAG语义检索不用复杂向量数据库纯Node实现文本向量化余弦相似度检索看完你能学会RAG语义搜索底层完整流程拆解调用大模型Embedding接口批量生成文本向量手写余弦相似度算法实现语义相似度排序本地JSON文件简易向量库开箱即用不用装Milvus/PG向量插件可直接运行完整Node代码拿来就能改造做博客站内搜索一、先搞懂RAG语义搜索核心原理1.1 什么是RAGRAG全称检索增强生成拆成三步Retrieval检索把知识库文本转向量根据问题语义匹配相似内容Augment增强把检索到的相关内容拼接进大模型提示词Generation生成大模型结合检索资料给出精准回答1.2 语义搜索和普通文字搜索核心区别传统搜索正则/数据库like只匹配文字字符无语义理解向量语义搜索把文本转为多维数字向量语义相近的文本向量距离更近通过余弦相似度计算匹配度整体流程预处理批量把所有文章标题、分类转为向量持久化存储用户搜索将用户输入问题转为向量相似度计算对比问题向量和所有文章向量按相似度从高到低排序截取TopN结果返回完成语义检索1.3 工具选型运行环境Node.js支持ES Module向量生成通义千问Embedding接口兼容OpenAI标准SDK向量存储本地JSON文件小型知识库无需向量库Milvus/PostgreSQL相似度算法手写余弦相似度函数无第三方依赖二、项目初始化与环境配置2.1 安装依赖npminstallopenai dotenv2.2 新建.env环境变量文件# 通义千问API密钥 DASHSCOPE_API_KEY你的key # 通义千问接口地址 DASHSCOPE_BASE_URLhttps://dashscope.aliyuncs.com/compatible-mode/v12.3 目录结构rag-search/ ├── .env ├── app.service.mjs # LLM接口客户端封装 ├── create-embedding.js # 批量生成文章向量脚本 ├── semantic-search.js # 语义检索交互脚本 └── data/ ├── posts.json # 原始文章数据源 └── posts-embedding.json # 存储向量的文件三、分步完整实战代码可直接复制运行3.1 封装LLM客户端 app.service.mjs统一封装Embedding请求实例全局复用避免重复初始化importOpenAIfromopenai;importdotenvfromdotenv;// 读取.env环境变量dotenv.config();// 导出全局客户端exportconstclientnewOpenAI({apiKey:process.env.DASHSCOPE_API_KEY,baseURL:process.env.DASHSCOPE_BASE_URL,});3.2 批量生成文本向量 create-embedding.js作用读取原始文章posts.json批量调用接口生成向量写入新文件持久化importfsfromfs/promises;import{client}from./app.service.mjs;// 文件路径配置constinputFilePath./data/posts.json;constoutputFilePath./data/posts-embedding.json;// 读取原始文章数据constrawTextawaitfs.readFile(inputFilePath,utf-8);constpostsJSON.parse(rawText);// 接口请求防抖防止调用超限constsleep(ms)newPromise(resolvesetTimeout(resolve,ms));constpostsWithEmbedding[];// 循环批量生成向量for(const{title,category}ofposts){// 拼接标题分类提升语义匹配精度constinputText标题${title}分类${category};constresponseawaitclient.embeddings.create({model:text-embedding-v4,input:inputText});// 保存文章信息对应向量postsWithEmbedding.push({title,category,embedding:response.data[0].embedding});// 间隔200ms避免接口限流awaitsleep(200);}// 将向量数据写入本地文件awaitfs.writeFile(outputFilePath,JSON.stringify(postsWithEmbedding,null,2));console.log(向量生成完成已持久化到posts-embedding.json);执行命令生成向量nodecreate-embedding.js3.3 手写余弦相似度算法 语义检索 semantic-search.js核心功能命令行输入搜索词向量化后计算所有文章相似度返回Top3匹配结果importfsfromfs/promises;import{client}from./app.service.mjs;importreadlinefromreadline;// 读取预生成好的向量文件constinputFilePath./data/posts-embedding.json;constdataawaitfs.readFile(inputFilePath,utf-8);constpostsJSON.parse(data);/** * 手写余弦相似度计算函数 * param {number[]} v1 向量1 * param {number[]} v2 向量2 * returns {number} 相似度区间[-1,1]数值越接近1语义越相似 */constcosineSimilarity(v1,v2){// 1. 计算向量点积constdotProductv1.reduce((acc,curr,i)acccurr*v2[i],0);// 2. 分别计算两个向量模长constlengthV1Math.sqrt(v1.reduce((acc,curr)acccurr*curr,0));constlengthV2Math.sqrt(v2.reduce((acc,curr)acccurr*curr,0));// 3. 余弦相似度公式constsimilaritydotProduct/(lengthV1*lengthV2);returnsimilarity;};// 命令行交互工具constrlreadline.createInterface({input:process.stdin,output:process.stdout});// 处理用户输入检索逻辑consthandleInputasync(searchWord){// 将用户搜索词转为向量constembedResawaitclient.embeddings.create({model:text-embedding-v4,input:searchWord});constsearchEmbeddingembedRes.data[0].embedding;// 遍历所有文章计算相似度并排序constresultListposts.map(item({...item,similarity:cosineSimilarity(searchEmbedding,item.embedding)}))// 升序排序后反转实现从高相似度到低相似度.sort((a,b)a.similarity-b.similarity).reverse()// 只取相似度最高前3条.slice(0,3).map((item,index)${index1}.${item.title}| 分类${item.category}).join(\n);console.log(\n 语义搜索结果 \n${resultList});rl.close();};// 启动输入交互rl.question(\n请输入你要搜索的内容,handleInput);启动检索命令nodesemantic-search.js四、实战演示效果假设posts.json存在两条数据[{title:Vue3组合式API实战教程,category:前端Vue},{title:低代码平台踩坑总结,category:前端工程化}]输入搜索词vue相关教程语义匹配会直接返回第一条Vue相关文章传统模糊匹配也能实现但如果输入模糊语义词前端框架开发指南普通like匹配不到向量检索依旧能精准命中Vue文章。输入搜索词低代码开发踩坑经历自动匹配低代码相关文章完美解决字面不匹配但语义一致的检索场景。五、开发踩坑提醒避坑重点加粗坑1Embedding接口不限流直接循环调用批量生成向量时不加sleep延时接口会直接触发限流报错。解决方案循环内增加200ms间隔企业级项目可改为批量接口、异步队列处理。坑2相似度排序逻辑写反代码中sort默认升序直接取前3会拿到相似度最低的数据。解决方案排序后调用.reverse()反转数组再截取TopN。坑3只单独向量化标题丢失分类语义只传入标题生成向量相同标题不同分类的内容无法区分匹配精度大幅下降。解决方案拼接标题分类组合文本作为Embedding入参。坑4JSON文件存储向量只适合小型知识库文章上千条后全量循环计算相似度会卡顿。解决方案数据量大替换Milvus/PG向量数据库数据库内置向量索引无需全量遍历计算。坑5未统一Embedding模型生成知识库向量和搜索词向量使用不同模型向量维度不一致相似度计算报错。解决方案两处统一使用text-embedding-v4模型。六、方案扩展优化方向替换向量数据库数据量超千条接入Milvus、PostgreSQL向量插件建立向量索引大幅提升检索速度前端接入封装接口前端传搜索词后端执行向量检索做成博客站内搜索功能增强RAG问答拿到检索结果后把文章内容传入大模型实现问答而非单纯列表检索缓存优化缓存高频搜索词向量减少重复调用Embedding接口消耗阈值过滤新增相似度阈值低于0.6的结果直接过滤避免无关内容展示七、整体总结传统关键词匹配只能识别字面向量语义搜索能理解文本含义解决模糊检索痛点整套RAG语义检索分为文本向量化持久化、问题向量化、余弦相似度匹配、结果排序四步小型项目无需重型向量库本地JSON手写余弦相似度就能快速落地代码无复杂依赖基于原生Node.js实现前端、Node后端都能直接复用改造。