用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 有成本,对于高频场景可以预计算 + 缓存
相关推荐
程序员黑豆2 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
小虎AI生活4 小时前
WorkBuddy 资料库架构解析:MD + CSV + HTML 三种载体的"流水线"设计
ai编程
咖啡星人k5 小时前
想私有化部署 AI 开发平台?MonkeyCode 给出的答案是开源 + 离线
人工智能·大模型·ai编程·monkeycode
神奇霸王龙7 小时前
Agent 准入门控屠夫:5 旗舰 4 维度评估
linux·运维·数据库·ai·ai作画·agent·ai编程
安全指北针8 小时前
AI编程工具供应链安全实战:当你的AI编码助手成为跳板
人工智能·安全·ai编程
tealcwu8 小时前
【已解决】Gemini突然提示不支持所在国家?
ai编程
ZzT8 小时前
把 AI Coding 工作流做成插件:资产、契约与运行时的三层拆分
aigc·openai·ai编程
程序员老刘11 小时前
Flutter 3.47 更新要点:Material 正式分家,老项目先别急着更新
flutter·ai编程·客户端
全栈弄潮儿12 小时前
一周总结:AI 编程入门最重要的 3 个原则
chatgpt·openai·ai编程
技术传感器13 小时前
让 Hermes 自动完成一个软件需求:从 Issue 到 PR 的 AI 开发流水线
人工智能·架构·aigc·需求分析·issue