一、传统文本检索的痛点:为什么需要 RAG 语义搜索
日常我们做站内文章检索,最原始的方案是精确字符串匹配 :用数据库like '%vue%'、正则匹配关键词,这种方式存在明显缺陷:
- 只能匹配字面相同词汇,无法理解语义:例如搜索「马铃薯做法」,无法命中「酸辣土豆丝教程」;
- 关键词同义词、近义词完全无法关联,检索召回率极低;
- 无法区分上下文语义,多义词容易出现无关结果。
RAG(Retrieval-Augmented Generation,检索增强生成)的核心思路就是向量语义检索,解决传统模糊匹配的短板:
- Retrival(检索):将文本转为向量,通过向量相似度匹配语义相近内容;
- Augment(增强):把检索到的相关原文作为上下文,交给大模型生成回答;
- Generate(生成):大模型结合检索素材输出精准结果。
向量检索的底层逻辑:把文字通过 Embedding 模型转为高维数字向量,语义相近的文本,向量在空间上距离更近,通过余弦相似度计算向量距离,就能实现语义匹配。
行业落地中,成熟方案会使用 Milvus、PGVector 这类向量数据库存储海量向量,本文先基于 Node.js + 本地 JSON 文件实现轻量化 RAG 检索,完整覆盖「文本向量化存储→向量相似度计算→命令行语义检索」全流程。
二、整体技术方案架构
1. 依赖与工具
- 运行环境:Node.js(支持 ES6 Promise、顶层 await)
- 向量模型:阿里云通义 Embedding
text-embedding-v4(兼容 OpenAI SDK 调用) - 文件处理:Node 内置
fs/promises(Promise 异步文件读写) - 交互工具:
readline实现命令行问答交互 - 环境管理:
dotenv读取 API 密钥 - 向量计算:原生 JS 实现余弦相似度算法
2. 流程分为两大阶段
- 离线预处理:原始文本批量向量化 读取原始文章
posts.json,调用 Embedding 接口将标题 + 分类转为向量,写入posts-embedding.json持久化存储,避免每次检索重复调用接口。 - 在线检索:用户输入语义匹配用户输入查询文本,实时生成查询向量,和本地所有预存向量计算余弦相似度,按相似度排序返回 Top3 相关文章。
三、代码分层拆解实现
3.1 统一封装大模型客户端(app.service.mjs)
统一封装 Embedding 请求实例,全局复用,分离配置与业务逻辑,适配大型项目分层规范:
javascript
运行
javascript
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();
// 导出客户端供其他模块复用
export const client = new OpenAI({
apiKey: process.env.VITE_QWEN_API_KEY,
// 阿里云通义兼容OpenAI接口地址
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
})
3.2 离线批量向量化脚本:生成向量库文件
原始素材data/posts.json仅存储文章标题、分类,无向量数据,执行脚本批量生成向量并落地本地文件:
javascript
运行
javascript
import fs from 'fs/promises';
import {client} from './app.service.mjs';
// 文件路径定义
const inputFilePath = './data/posts.json';
const outputFilePath = './data/posts-embedding.json';
// 简单延时函数,防止接口调用超限
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 1. 读取原始文章数据
const rawText = await fs.readFile(inputFilePath, 'utf-8');
const posts = JSON.parse(rawText);
const postsWithEmbeddings = [];
// 2. 循环调用Embedding接口生成向量
for (const {title,category} of posts) {
// 拼接文本作为输入,融合标题+分类提升语义精度
const response = await client.embeddings.create({
model: "text-embedding-v4",
input: `标题:${title},分类:${category}`
});
// 存储原文信息+对应向量
postsWithEmbeddings.push({
title,
category,
embedding: response.data[0].embedding
});
await sleep(200); // 限流,避免请求过快报错
}
// 3. 将带向量的数据写入本地JSON文件持久化
console.log(`向量化完成, 共${postsWithEmbeddings.length}条数据`);
await fs.writeFile(outputFilePath, JSON.stringify(postsWithEmbeddings, null, 2));
console.log(postsWithEmbeddings[0]);
执行脚本后会生成posts-embedding.json,每条数据都附带高维向量数组,作为本地简易向量库。
3.3 核心工具:余弦相似度计算函数
向量相似度是语义匹配的核心,使用余弦相似度衡量两个向量的语义接近程度,值域[-1,1],数值越接近 1 代表语义越相似:
javascript
运行
javascript
/**
* 计算两个向量的余弦相似度
* @param {number[]} v1 查询向量
* @param {number[]} v2 库内存储向量
* @returns {number} 相似度分值
*/
const cosineSimilarity = (v1, v2) => {
// 1. 向量点积
const dotProduct = v1.reduce((acc, curr, i) => acc + curr * v2[i], 0);
// 2. 计算两个向量模长
const lengthV1 = Math.sqrt(v1.reduce((acc, curr) => acc + curr * curr, 0));
const lengthV2 = Math.sqrt(v2.reduce((acc, curr) => acc + curr * curr, 0));
// 3. 余弦相似度公式
const similarity = dotProduct / (lengthV1 * lengthV2);
return similarity;
};
3.4 在线语义检索:命令行交互查询
读取预生成的向量文件,接收用户输入,实时计算向量并匹配相似文章:
javascript
运行
javascript
import fs from 'fs/promises';
import {client} from './app.service.mjs';
import readline from 'readline'; // Node内置命令行交互模块
const inputFilePath = './data/posts-embedding.json';
// 读取预向量化完成的数据
const fileData = await fs.readFile(inputFilePath, 'utf-8');
const posts = JSON.parse(fileData);
// 初始化命令行输入输出
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 处理用户检索输入
const handleInput = async (queryText) => {
// 将用户查询语句转为向量
const response = await client.embeddings.create({
model: "text-embedding-v4",
input: queryText
});
const queryEmbedding = response.data[0].embedding;
// 1. 遍历所有数据,计算每条数据和查询语句的相似度
// 2. 按相似度从高到低排序,截取前3条最相关结果
// 3. 格式化输出标题与分类
const searchResult = posts
.map(item => ({
...item,
similarity: cosineSimilarity(queryEmbedding, item.embedding)
}))
.sort((a,b) => b.similarity - a.similarity) // 降序排序
.slice(0, 3)
.map((item,index)=>`${index+1}. ${item.title} | ${item.category}`)
.join('\n');
console.log(`\n===== 语义搜索结果 =====\n${searchResult}`);
// 循环接收下一次输入,持续检索
rl.question('请输入你要搜索的内容: ', handleInput)
};
// 启动交互,等待用户输入
rl.question('请输入你要搜索的内容: ', handleInput)
四、运行流程与使用说明
-
环境准备 新建
.env文件配置阿里云通义密钥:env
iniVITE_QWEN_API_KEY=你的通义千问API密钥 -
原始素材准备 在
data/posts.json存放文章列表,格式示例:json
css[ {"title":"Vue3组合式API实战教程","category":"前端Vue"}, {"title":"马铃薯家常烹饪方法","category":"美食菜谱"}] -
离线生成向量库 执行向量化脚本,批量生成向量存入
posts-embedding.json; -
启动语义检索运行检索脚本,在命令行输入查询词,即可基于语义匹配内容:
- 输入
vue入门教程,可精准匹配 Vue 相关文章; - 输入
土豆怎么做,可匹配标题含马铃薯的美食文章;实现传统模糊搜索无法做到的语义关联。
- 输入
五、本地 JSON 向量库的局限与生产级优化方案
当前方案仅使用本地 JSON 存储向量,适合学习演示,线上掘金、资讯类生产系统存在明显短板:
- 性能瓶颈:数据量增大后,全量遍历计算相似度耗时极高;
- 无索引优化:缺少向量索引,无法实现百万级数据快速检索;
- 持久化能力弱:文件读写并发差,不支持多服务共享。
生产级优化方向(对应 README 文档方案)
-
引入专业向量数据库
- Milvus:专用向量检索引擎,支持海量向量分片、索引、距离检索;
- PostgreSQL+PGVector:关系数据库拓展向量存储,兼顾业务数据与向量;
-
预计算全量向量入库服务启动时批量 Embedding 写入向量库,检索时直接调用向量库接口完成相似度计算,无需手动循环计算;
-
完整 RAG 链路拓展在检索到相关文章后,将文章内容拼接为上下文 Prompt,传入大模型,实现「检索素材 + AI 生成回答」完整 RAG 能力;
-
接口化改造将命令行交互改为 HTTP 接口,前端页面调用实现网页语义搜索,对标掘金站内智能搜索功能。
六、总结
本文基于 Node.js 完成了一套极简 RAG 语义检索 Demo,完整覆盖文本向量化、余弦相似度计算、本地向量存储、交互式语义查询四大核心环节,直观对比了传统关键词匹配与向量语义检索的差异。
通过 Embedding 模型将自然语言转为数字向量,利用向量空间距离表达语义相似度,是 RAG 检索的底层核心逻辑。本地 JSON 方案适合入门学习,实际业务中替换 Milvus、PGVector 向量数据库,即可落地生产可用的智能语义搜索系统,广泛应用于文档检索、知识库问答、社区文章搜索等场景