用RAG + Node.js 手搓一个语义搜索引擎,真香!

告别"Like %关键词%",让搜索真正理解你的意思

你有没有遇到过这种情况:在后台管理系统中搜"马铃薯怎么做",结果却搜出一堆包含"土豆"但完全不相关的文章?或者想找"Vue相关的内容",却只能靠标题精确匹配,漏掉了大量同义但表述不同的优质内容?

这就是传统关键词搜索的痛点------它只看字面,不理解语义。

今天,我们就用 RAG(检索增强生成) 的思路,配合 Node.js + 向量数据库(pgvector/Milvus),手搓一个语义搜索引擎。代码量不大,但效果立竿见影。


一、RAG 是什么?先拆解一下

RAG 全称 Retrieval-Augmented Generation ,但在这个场景里,我们主要用它的检索(Retrieval) 部分,不一定要接大模型生成。

它的核心流程就三步:

步骤 含义 我们的做法
Retrieval(检索) 从知识库中找回最相关的内容 向量相似度匹配
Augment(增强) 把检索结果拼进上下文 可选,这里暂不展开
Generate(生成) 让LLM基于上下文回答 后续可接大模型

今天我们的目标是:输入"酸辣土豆丝的做法",能精准召回"马铃薯怎么做好吃"这条内容,哪怕它不包含"酸辣"和"土豆丝"这两个词。


二、整体架构:从数据到搜索

我们项目的目录结构长这样(别嫌乱,真实项目就是这么朴实无华):

text

bash 复制代码
posts-demo/
├── data/
│   └── posts.json          # 原始数据(标题 + 分类)
├── app.service.mjs         # 封装 OpenAI 客户端(复用)
├── create-embedding.mjs    # 离线生成向量数据
├── semantic-search.mjs     # 命令行交互式搜索
├── .env                    # 环境变量(API Key)
└── package.json

一句话概括流程:

数据 → Embedding(向量化)→ 存储 → 用户问题 → 向量化 → 余弦相似度匹配 → 返回 TopK


三、第一步:封装 LLM 客户端(app.service.mjs)

为了方便复用,我们单独抽出一个 app.service.mjs,负责管理 OpenAI 兼容的 Embedding 服务

javascript

arduino 复制代码
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();

// 大型项目的风骨:app 应用 service 获取 LLM 服务
// 模块化输出 client,方便复用
export const client = new OpenAI({
  apiKey: process.env.DASHSCORE_API_KEY,
  baseURL: process.env.DASHSCORE_API_BASE_URL,
});

这里用的是 DashScope 的 text-embedding-v4 模型,你也可以替换成 OpenAI 的 text-embedding-ada-002 或其他。


四、第二步:给数据做 Embedding(create-embedding.mjs)

我们需要把 posts.json 里的每一条内容(标题 + 分类)转成高维向量

javascript

javascript 复制代码
import { client } from './app.service.mjs';
import fs from 'fs/promises';

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const posts = JSON.parse(await fs.readFile('./data/posts.json', 'utf-8'));
const postsWithEmbedding = [];

for (const { title, category } of posts) {
  console.log(title, category, 'embedding 中...');
  
  const response = await client.embeddings.create({
    model: 'text-embedding-v4',
    input: `标题:${title},分类:${category}`,
  });

  postsWithEmbedding.push({
    title,
    category,
    embedding: response.data[0].embedding,
  });

  await sleep(200); // 限频,避免被 API 拒
}

await fs.writeFile(
  './data/posts-embedding.json',
  JSON.stringify(postsWithEmbedding, null, 2)
);

console.log('✅ 向量数据写入成功!');

⚠️ 注意:这里我们特意把 titlecategory 拼在一起作为 input,让向量同时包含主题和分类信息,语义更丰富。


五、第三步:语义搜索的核心(semantic-search.mjs)

5.1 加载向量数据 + 定义余弦相似度

javascript

javascript 复制代码
import { client } from './app.service.mjs';
import fs from 'fs/promises';

const data = await fs.readFile('./data/posts-embedding.json', 'utf-8');
const posts = JSON.parse(data);

// 余弦相似度:衡量两个向量在高维空间的"方向一致性"
const cosineSimilarity = (v1, v2) => {
  const dotProduct = v1.reduce((acc, curr, i) => acc + curr * v2[i], 0);
  const lengthV1 = Math.sqrt(v1.reduce((acc, curr) => acc + curr * curr, 0));
  const lengthV2 = Math.sqrt(v2.reduce((acc, curr) => acc + curr * curr, 0));
  return dotProduct / (lengthV1 * lengthV2);
};

5.2 命令行交互 + 搜索逻辑

我们用 Node.js 内置的 readline 模块实现一个问答式交互界面

javascript

javascript 复制代码
import readline from 'readline';

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const handleInput = async (answer) => {
  // 1. 把用户的问题也向量化
  const response = await client.embeddings.create({
    model: 'text-embedding-v4',
    input: answer,
  });
  const { embedding } = response.data[0];

  // 2. 计算每条数据的相似度,排序,去重,取 Top3
  const results = posts
    .map(item => ({
      ...item,
      similarity: cosineSimilarity(item.embedding, embedding),
    }))
    .sort((a, b) => b.similarity - a.similarity)
    .filter((item, index, arr) => 
      index === arr.findIndex(t => t.title === item.title)
    )
    .slice(0, 3)
    .map((item, index) => `${index+1}. ${item.title} (${item.category})`)
    .join('\n');

  console.log(`\n🔍 搜索结果:\n${results}`);
  rl.close();
};

rl.question('\n请输入你要搜索的内容:', handleInput);

六、效果对比:传统搜索 vs 语义搜索

用户输入 传统 SQL (LIKE '%vue%') 语义搜索
"Vue 相关的内容" 只能匹配标题包含 "vue" 的 能召回 "Vue3 组合式 API 实战"、"Pinia 状态管理" 等语义相关内容
"酸辣土豆丝的做法" 必须含 "酸辣" 或 "土豆丝" 能召回 "马铃薯怎么做好吃"
"怎么学前端" 可能匹配不到 "前端工程化" 能召回所有涉及前端学习路径的内容

核心区别 :语义搜索把文本映射到高维空间,用余弦距离衡量"意思的远近",而不是"字面的重合"。


七、进阶:接入向量数据库(生产环境必备)

在 demo 里我们直接 JSON.parse 全量加载,数据量大了肯定扛不住。

生产环境建议接入:

  • pgvector(PostgreSQL 扩展)------ 适合中小型项目,沿用 SQL 习惯
  • Milvus ------ 专门为向量检索设计的分布式数据库
  • Qdrant / Weaviate ------ 云原生友好

我们项目里已经在规划这一步了(看图里的 milvuspostgresql 标签),后续会补上。


八、总结与思考

通过这个项目,我们实际上手了:

  1. RAG 检索环节的完整实现
  2. ✅ 用 Embedding 模型把自然语言变成向量
  3. 余弦相似度的计算与排序
  4. ✅ 命令行交互式搜索的构建
  5. ✅ 模块化设计思想(app.service.mjs 复用客户端)

一点小思考

  • Embedding 质量决定搜索上限 :选一个好的向量模型(如 text-embedding-v4)比调参更重要。
  • 去重和过滤策略:实际业务中还要考虑分类过滤、权限过滤等。
  • 延迟与成本的平衡:实时 Embedding 调用 API 有成本,对于高频场景可以预计算 + 缓存
相关推荐
AINative软件工程13 小时前
MCP Server 权限边界工程实践:OAuth、最小权限与工具沙箱,别让 Agent 拿到整台机器
架构·llm·ai编程
深念Y13 小时前
AI编程Agent工具定义对比分析
agent·ai编程·开源项目·工具·tool·hermes·ccsiwtch
fanstuck19 小时前
1M 上下文能怎么用?我用 Seed Evolving 做了一个招标文件版本差异审查器
服务器·人工智能·数据分析·开源·aigc
东小西1 天前
第8篇:《白嫖社区生态:一行配置接入GitHub的MCP Server,AI直接读我的代码仓库》
openai·ai编程
雨辰AI1 天前
全集实战:企业级大模型服务化部署全栈指南|FastAPI 封装 + Nginx 负载均衡 + 高可用架构 从单机到生产一步到位
人工智能·ai·负载均衡·fastapi·ai编程
chaors1 天前
DeepResearchSystem 0x01:Agent 基础
langchain·aigc·ai编程
sweet丶1 天前
移动端轻量级AI Agent自动化UI验证:sim-use
ai编程
东小西1 天前
第7篇:《工具的USB接口:我把Function Calling升级成了MCP》
openai·ai编程
腻害兔1 天前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:CRM 客户关系模块深度解析——从线索到回款,一套完整的 B2B 销售闭环是怎么搭的?
java·前端·javascript·vue.js·产品经理·ai编程
大数据点灯人1 天前
【AI编程】Vibe Coding 模型选型:越贵越好吗?效果/速度/成本平衡指南
编程·ai编程·claude·codex·vibe coding