前言:你还在用关键词Like搜索?痛点拉满
做前端/Node后端写站内搜索时,大家基本都踩过文字匹配的坑:
- 搜「Vue开发技巧」,文章标题只写了「前端框架Vue实战」,关键词匹配直接搜不到
- 搜「土豆做法」,数据库like模糊匹配匹配不出「酸辣土豆丝教程」
- 海量文章数据,全量模糊查询性能爆炸,多关键词分词逻辑写起来巨繁琐
传统文字匹配只认字面字符,不懂语义。
今天带你从零手写一套轻量RAG语义检索,不用复杂向量数据库,纯Node实现文本向量化+余弦相似度检索,看完你能学会:
- RAG语义搜索底层完整流程拆解
- 调用大模型Embedding接口批量生成文本向量
- 手写余弦相似度算法,实现语义相似度排序
- 本地JSON文件简易向量库,开箱即用不用装Milvus/PG向量插件
- 可直接运行完整Node代码,拿来就能改造做博客站内搜索
一、先搞懂:RAG语义搜索核心原理
1.1 什么是RAG
RAG全称:检索增强生成,拆成三步:
- 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_URL=https://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请求实例,全局复用,避免重复初始化
importOpenAIfrom'openai';importdotenvfrom'dotenv';// 读取.env环境变量dotenv.config();// 导出全局客户端exportconstclient=newOpenAI({apiKey:process.env.DASHSCOPE_API_KEY,baseURL:process.env.DASHSCOPE_BASE_URL,});3.2 批量生成文本向量 create-embedding.js
作用:读取原始文章posts.json,批量调用接口生成向量,写入新文件持久化
importfsfrom'fs/promises';import{client}from'./app.service.mjs';// 文件路径配置constinputFilePath='./data/posts.json';constoutputFilePath='./data/posts-embedding.json';// 读取原始文章数据constrawText=awaitfs.readFile(inputFilePath,'utf-8');constposts=JSON.parse(rawText);// 接口请求防抖,防止调用超限constsleep=(ms)=>newPromise(resolve=>setTimeout(resolve,ms));constpostsWithEmbedding=[];// 循环批量生成向量for(const{title,category}ofposts){// 拼接标题+分类,提升语义匹配精度constinputText=`标题:${title},分类:${category}`;constresponse=awaitclient.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匹配结果
importfsfrom'fs/promises';import{client}from'./app.service.mjs';importreadlinefrom'readline';// 读取预生成好的向量文件constinputFilePath='./data/posts-embedding.json';constdata=awaitfs.readFile(inputFilePath,'utf-8');constposts=JSON.parse(data);/** * 手写余弦相似度计算函数 * @param {number[]} v1 向量1 * @param {number[]} v2 向量2 * @returns {number} 相似度,区间[-1,1],数值越接近1语义越相似 */constcosineSimilarity=(v1,v2)=>{// 1. 计算向量点积constdotProduct=v1.reduce((acc,curr,i)=>acc+curr*v2[i],0);// 2. 分别计算两个向量模长constlengthV1=Math.sqrt(v1.reduce((acc,curr)=>acc+curr*curr,0));constlengthV2=Math.sqrt(v2.reduce((acc,curr)=>acc+curr*curr,0));// 3. 余弦相似度公式constsimilarity=dotProduct/(lengthV1*lengthV2);returnsimilarity;};// 命令行交互工具constrl=readline.createInterface({input:process.stdin,output:process.stdout});// 处理用户输入检索逻辑consthandleInput=async(searchWord)=>{// 将用户搜索词转为向量constembedRes=awaitclient.embeddings.create({model:'text-embedding-v4',input:searchWord});constsearchEmbedding=embedRes.data[0].embedding;// 遍历所有文章,计算相似度并排序constresultList=posts.map(item=>({...item,similarity:cosineSimilarity(searchEmbedding,item.embedding)}))// 升序排序后反转,实现从高相似度到低相似度.sort((a,b)=>a.similarity-b.similarity).reverse()// 只取相似度最高前3条.slice(0,3).map((item,index)=>`${index+1}.${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文章。 - 输入搜索词:
低代码开发踩坑经历
自动匹配低代码相关文章,完美解决字面不匹配但语义一致的检索场景。
五、开发踩坑提醒(避坑重点加粗)
坑1:Embedding接口不限流直接循环调用
批量生成向量时不加sleep延时,接口会直接触发限流报错。
解决方案:循环内增加200ms间隔,企业级项目可改为批量接口、异步队列处理。
坑2:相似度排序逻辑写反
代码中sort默认升序,直接取前3会拿到相似度最低的数据。
解决方案:排序后调用.reverse()反转数组,再截取TopN。
坑3:只单独向量化标题,丢失分类语义
只传入标题生成向量,相同标题不同分类的内容无法区分,匹配精度大幅下降。
解决方案:拼接标题+分类组合文本作为Embedding入参。
坑4:JSON文件存储向量只适合小型知识库
文章上千条后,全量循环计算相似度会卡顿。
解决方案:数据量大替换Milvus/PG向量数据库,数据库内置向量索引,无需全量遍历计算。
坑5:未统一Embedding模型
生成知识库向量和搜索词向量使用不同模型,向量维度不一致,相似度计算报错。
解决方案:两处统一使用text-embedding-v4模型。
六、方案扩展优化方向
- 替换向量数据库:数据量超千条,接入Milvus、PostgreSQL向量插件,建立向量索引,大幅提升检索速度
- 前端接入:封装接口,前端传搜索词,后端执行向量检索,做成博客站内搜索功能
- 增强RAG问答:拿到检索结果后,把文章内容传入大模型,实现问答而非单纯列表检索
- 缓存优化:缓存高频搜索词向量,减少重复调用Embedding接口消耗
- 阈值过滤:新增相似度阈值,低于0.6的结果直接过滤,避免无关内容展示
七、整体总结
- 传统关键词匹配只能识别字面,向量语义搜索能理解文本含义,解决模糊检索痛点;
- 整套RAG语义检索分为:文本向量化持久化、问题向量化、余弦相似度匹配、结果排序四步;
- 小型项目无需重型向量库,本地JSON+手写余弦相似度就能快速落地;
- 代码无复杂依赖,基于原生Node.js实现,前端、Node后端都能直接复用改造。