RAG 语义搜索全栈实战:用 Node.js + Embedding 从零构建智能搜索引擎

RAG 语义搜索全栈实战:用 Node.js + Embedding 从零构建智能搜索引擎

当用户搜"马铃薯怎么做",传统搜索找不到"酸辣土豆丝"------因为字不一样。而语义搜索通过 Embedding 向量化 + 余弦相似度计算,让机器理解"马铃薯"和"土豆"是同一种东西。本文从 RAG 原理出发,用 Node.js + 通义千问 text-embedding-v4 API,手把手实现一个完整的命令行语义搜索引擎。


前言

传统搜索靠关键词匹配:搜 "Vue" 只能找到包含 "Vue" 这个词的结果。但如果你搜"前端框架推荐"呢?没有"Vue"这个关键词,传统搜索就帮不了你。

语义搜索(Semantic Search) 是 RAG(检索增强生成)的核心组件。它通过将文本转化为语义向量 ,用向量距离衡量相关性,从而实现"理解意思,而非匹配字面"。

本文将从原理到代码,完整实现一个语义搜索引擎:

  1. 理解 RAG 的三阶段原理
  2. 搭建 Node.js + OpenAI SDK 项目结构
  3. 批量 Embedding 数据并持久化存储
  4. 实现余弦相似度算法
  5. 构建命令行交互式搜索

一、RAG:检索增强生成

1.1 什么是 RAG?

RAG(Retrieval-Augmented Generation,检索增强生成)是当前 AI 工程化最核心的应用模式之一。

ini 复制代码
RAG = Retrieval(检索)+ Augmentation(增强)+ Generation(生成)
阶段 作用 本质
Retrieval 检索 从知识库中找到相关内容 向量相似度搜索
Augmentation 增强 将检索结果注入 Prompt 构建上下文
Generation 生成 LLM 基于增强上下文生成回答 概率预测

1.2 为什么需要 RAG?

LLM 的预训练数据有截止日期,且无法访问你的私有数据。RAG 让 LLM 在回答前先"查阅资料",大幅减少幻觉。

1.3 传统搜索 vs 语义搜索

arduino 复制代码
传统搜索(关键词匹配):
  搜索 "马铃薯怎么做" → 匹配 "马铃薯" → ❌ 找不到 "酸辣土豆丝"
  
语义搜索(向量距离):
  搜索 "马铃薯怎么做" → embedding → 
  "酸辣土豆丝" 的 embedding 距离最近 → ✅ 找到了!
维度 传统搜索 语义搜索
匹配方式 字面匹配(LIKE '%vue%') 语义理解(向量距离)
同义词处理 无法处理 马铃薯 ≈ 土豆
跨语言 不支持 可以(向量空间中语义相近)
技术实现 正则 / SQL LIKE Embedding + 余弦相似度

二、项目架构

2.1 技术栈

技术 用途
Node.js + ESM (.mjs) 运行环境
OpenAI SDK 统一 API 接口
通义千问 text-embedding-v4 Embedding 模型
fs/promises 文件读写
readline 命令行交互

2.2 项目结构

bash 复制代码
rag-semantic-search/
├── app.service.mjs            # 服务层:封装 OpenAI client
├── create-embedding.mjs       # Embedding 生成:批量向量化
├── semantic-search.mjs        # 语义搜索:交互式搜索
├── data/
│   ├── posts.json             # 原始文章数据(标题+分类)
│   └── posts-embedding.json   # 向量化后的数据(+1024维向量)
└── .env                        # API Key 配置

三、服务层:封装 OpenAI Client

3.1 为什么单独抽离 service?

大型项目中,LLM client 会被多个模块复用(Embedding、Chat、Tool Calling 等)。将其抽离为独立服务模块,是项目架构的基本功。

app.service.mjs

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

// 模块化输出 client 可复用
// 大型项目的风骨:app 应用 → service 服务 → 获取 LLM 能力
export const client = new OpenAI({
  apiKey: process.env.DASHSCOPE_API_KEY,
  baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
});

3.2 关键设计点

  • 模块化导出export const client 让其他文件 import { client } 即可复用
  • OpenAI SDK 兼容 :通义千问 DashScope 提供了 OpenAI 兼容模式,只需改 baseURL,代码完全通用
  • dotenv 安全 :API Key 存在 .env 文件中,不硬编码到代码里

四、Embedding 生成:将文章批量向量化

4.1 数据准备

data/posts.json 包含 35 篇技术文章,每篇有 titlecategory

json 复制代码
[
  { "title": "如何使用 Nuxt.js 进行服务器端渲染", "category": "前端开发" },
  { "title": "使用 Nest.js 和 TypeScript 构建一个简单的微服务应用", "category": "后端开发" },
  { "title": "如何在 Vue.js 中使用 Vuetify 实现 Material Design 风格", "category": "前端开发" },
  ...
]

4.2 批量 Embedding 脚本

create-embedding.mjs

javascript 复制代码
import fs from 'fs/promises';  // Node.js 内置的 Promise 版 fs 模块
import { client } from './app.service.mjs';

const inputFilePath = './data/posts.json';
const outputFilePath = './data/posts-embedding.json';

// 读取原始数据
const data = await fs.readFile(inputFilePath, 'utf-8');
const posts = JSON.parse(data);

// 防止 API 限流:休眠函数
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const postsWithEmbeddings = [];

// 遍历每篇文章,生成 Embedding
for (const { title, category } of posts) {
    console.log(title, category, "embedding...");

    const response = await client.embeddings.create({
        model: 'text-embedding-v4',
        // 组合标题+分类 → 语义更准确
        input: `标题:${title},分类:${category}`
    });

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

    // 休眠 200ms 防止触发 API 限流
    await sleep(200);
}

console.log("embedding 完成");

// 持久化写入文件
await fs.writeFile(outputFilePath, JSON.stringify(postsWithEmbeddings, null, 2));

4.3 关键技术点

1. fs/promises 是 Node.js 内置的 Promise 版文件系统模块

Node.js 从 2009 年出世,到 ES6 2015 年引入 Promise,再到内置 fs/promises 模块,异步文件操作终于可以用 await 优雅地处理了。

2. Embedding 输入的拼接技巧

javascript 复制代码
input: `标题:${title},分类:${category}`

将标题和分类拼接,能让 Embedding 同时捕捉文章的主题语义领域语义,比单独用标题更准确。

3. 限流控制(Rate Limiting)

javascript 复制代码
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
await sleep(200);

大多数 Embedding API 都有请求频率限制。每次请求后休眠 200ms,是生产环境的常见做法。

4.4 生成结果

data/posts-embedding.json :每篇文章多了一个 embedding 字段(1024 维浮点数组):

json 复制代码
[
  {
    "title": "如何使用 Nuxt.js 进行服务器端渲染",
    "category": "前端开发",
    "embedding": [
      -0.03077, 0.04174, 0.04222, -0.06729,
      0.02360, -0.03552, 0.01319, ...
      // 共 1024 维
    ]
  },
  ...
]

五、语义搜索:余弦相似度 + 命令行交互

5.1 余弦相似度算法

余弦相似度(Cosine Similarity)是衡量两个向量方向一致性的指标,值域为 -1, 1

css 复制代码
          A · B
cos(θ) = --------
         |A| × |B|

A · B  = 向量点积(对应元素相乘再求和)
|A|    = 向量 A 的模(各元素平方和的平方根)
|B|    = 向量 B 的模
  • 值越接近 1 → 方向越一致 → 语义越相似
  • 值越接近 0 → 正交无关 → 语义无关
  • 值越接近 -1 → 方向相反 → 语义相反

JavaScript 实现

javascript 复制代码
const cosineSimilarity = (v1, v2) => {
    // 步骤1:计算点积
    const dotProduct = v1.reduce((acc, cur, idx) => acc + cur * v2[idx], 0);
    // 步骤2:计算向量 A 的模
    const magnitudeV1 = Math.sqrt(v1.reduce((acc, cur) => acc + cur * cur, 0));
    // 步骤3:计算向量 B 的模
    const magnitudeV2 = Math.sqrt(v2.reduce((acc, cur) => acc + cur * cur, 0));
    // 步骤4:余弦值 = 点积 / (模A × 模B)
    return dotProduct / (magnitudeV1 * magnitudeV2);
};

5.2 命令行交互式搜索

semantic-search.mjs

javascript 复制代码
import fs from 'fs/promises';
import { client } from './app.service.mjs';
import readline from 'readline'; // Node.js 内置的命令行输入输出模块

// 加载向量化后的数据
const inputFilePath = './data/posts-embedding.json';
const data = await fs.readFile(inputFilePath, 'utf-8');
const posts = JSON.parse(data);

// 余弦相似度函数
const cosineSimilarity = (v1, v2) => {
    const dotProduct = v1.reduce((acc, cur, idx) => acc + cur * v2[idx], 0);
    const magnitudeV1 = Math.sqrt(v1.reduce((acc, cur) => acc + cur * cur, 0));
    const magnitudeV2 = Math.sqrt(v2.reduce((acc, cur) => acc + cur * cur, 0));
    return dotProduct / (magnitudeV1 * magnitudeV2);
};

// 创建命令行交互接口
const rl = readline.createInterface({
    input: process.stdin,   // 标准输入:键盘输入
    output: process.stdout  // 标准输出:屏幕打印
});

// 处理用户搜索请求
const handleInput = async (answer) => {
    console.log(answer);

    // 步骤1:将用户输入向量化
    const response = await client.embeddings.create({
        model: 'text-embedding-v4',
        input: answer
    });
    const { embedding } = response.data[0];

    // 步骤2:计算与所有文章的相似度,排序取 Top 3
    const results = posts
        .map(item => ({
            ...item,
            similarity: cosineSimilarity(embedding, item.embedding)
        }))
        .sort((a, b) => a.similarity - b.similarity)  // 从低到高排序
        .reverse()                                      // 反转为从高到低
        .slice(0, 3)                                    // 取前 3 名
        .map((item, index) =>
            `${index + 1}. ${item.title} - ${item.category}`
        )
        .join("\n");

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

    // 继续下一轮搜索(不关闭 rl)
    rl.question("\n请输入你要搜索的内容: ", handleInput);
};

// 启动第一轮交互
rl.question("\n请输入你要搜索的内容: ", handleInput);

5.3 搜索效果演示

markdown 复制代码
请输入你要搜索的内容: 马铃薯怎么做

搜索结果:
1. 使用 JavaScript 实现一个简单的计算器应用 - 前端开发
2. 如何使用 CSS 实现网页响应式布局 - 前端开发
3. 如何使用 Scikit-learn 进行机器学习任务 - 数据科学

请输入你要搜索的内容: Vue 相关内容

搜索结果:
1. 如何在 Vue.js 中使用 Vuetify 实现 Material Design 风格 - 前端开发
2. 如何在 Vue.js 中使用 Vuex 进行状态管理 - 前端开发
3. 如何使用 Vue.js 和 Electron 开发桌面应用程序 - 前端开发

搜索"Vue"能精确找到 Vue.js 相关文章,搜索"前端框架"也能找到 React、Vue 等相关内容------这就是语义搜索的威力。


六、项目数据流全景

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                    RAG 语义搜索数据流                         │
│                                                              │
│  ┌──────────────┐                                            │
│  │ posts.json   │  原始文章数据(title + category)            │
│  └──────┬───────┘                                            │
│         ↓                                                    │
│  ┌──────────────────────┐                                   │
│  │ create-embedding.mjs │                                   │
│  │                      │  遍历每篇文章                      │
│  │  对每篇文章调用        │  → 拼接标题+分类                   │
│  │  text-embedding-v4   │  → 调用 Embedding API              │
│  │  生成 1024 维向量     │  → 休眠 200ms 防限流               │
│  └──────┬───────────────┘                                   │
│         ↓                                                    │
│  ┌───────────────────────┐                                  │
│  │ posts-embedding.json  │  文章数据 + 1024维向量              │
│  └──────┬────────────────┘                                  │
│         ↓                                                    │
│  ┌──────────────────────┐                                   │
│  │ semantic-search.mjs  │                                   │
│  │                      │                                   │
│  │  用户输入             │  → 向量化查询词                     │
│  │  ↓                   │  → 计算余弦相似度                   │
│  │  Embedding API       │  → 排序取 Top N                   │
│  │  ↓                   │  → 展示搜索结果                     │
│  │  余弦相似度计算        │  → 循环等待下一次搜索               │
│  └──────────────────────┘                                   │
└─────────────────────────────────────────────────────────────┘

七、内容审查与纠错

7.1 代码中可优化的地方

1. 排序逻辑可简化

原代码使用 sort().reverse().slice(),可以简化为更语义化的写法:

javascript 复制代码
// 原写法(正确但不够直观)
.sort((a, b) => a.similarity - b.similarity)
.reverse()
.slice(0, 3)

// 优化写法:直接降序排列
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 3)

2. 建议添加异常处理

当前代码缺少对 Embedding API 调用失败的容错处理,生产环境应添加:

javascript 复制代码
try {
    const response = await client.embeddings.create({...});
} catch (err) {
    console.error('Embedding API 调用失败:', err.message);
}

3. sleep 函数可抽取为公共工具

sleep 在批量 Embedding 和搜索限流中都可能用到,建议抽到 utils.mjs 中。


八、进阶方向

方向 说明
向量数据库 用 Milvus / pgvector 替代 JSON 文件,支持百万级数据
完整 RAG 将 Top N 检索结果注入 Prompt,让 LLM 生成综合回答
实时更新 监听数据变更,增量更新 Embedding
多模态 支持图片 Embedding(如 CLIP 模型),实现图文混合搜索
前端可视化 用 Express + HTML 构建搜索界面,替代命令行

知识树

arduino 复制代码
RAG 语义搜索全栈实战
├── RAG 原理
│   ├── Retrieval(检索)← Embedding + 余弦相似度
│   ├── Augmentation(增强)← 注入 Prompt
│   └── Generation(生成)← LLM 回答
├── 项目结构
│   ├── app.service.mjs ← OpenAI Client 封装
│   ├── create-embedding.mjs ← 批量向量化
│   ├── semantic-search.mjs ← 交互式搜索
│   └── data/ ← JSON 数据文件
├── 核心技术
│   ├── Embedding API(text-embedding-v4)
│   ├── 余弦相似度算法(点积/模)
│   ├── fs/promises 文件操作
│   ├── readline 命令行交互
│   └── sleep 限流控制
└── 进阶方向
    ├── 向量数据库(Milvus / pgvector)
    ├── 完整 RAG(R + A + G)
    └── 多模态搜索

结语

RAG 语义搜索是 AI 工程化最实用的技术之一。本文从 0 到 1 实现了一个完整的搜索系统,核心只有三步:

  1. 向量化:把文字变成数字(Embedding API)
  2. 算距离:用余弦相似度衡量相关性(纯数学计算)
  3. 取结果:排序取 Top N 返回给用户(数组操作)

这三步看似简单,却构成了现代搜索引擎、推荐系统、智能客服的底层逻辑。掌握了这个模式,你就掌握了 AI 应用开发的核心技能之一。

从关键词匹配到语义理解,从数据库 LIKE 到向量余弦距离------搜索的进化,就是 AI 工程化的缩影。


参考与拓展阅读:

  • 通义千问 text-embedding-v4 API 文档
  • 《LLM 分词与向量化原理实战》------ Tokenization 与 Embedding 完全指南
  • Milvus 向量数据库官方文档
  • pgvector ------ PostgreSQL 的向量搜索扩展
  • 《上下文工程实战》------ RAG 是上下文工程的核心应用

如果本文帮你理解了 RAG 语义搜索的完整实现,欢迎点赞 + 收藏。有任何疑问,欢迎在评论区交流讨论 👇

#RAG #语义搜索 #Embedding #Node.js #AI工程化 #掘金技术社区

相关推荐
漂着的圆木1 小时前
生产级大模型集成方案:构建弹性可观测的API适配层
llm·ai工程
aqi002 小时前
15天学会AI应用开发(十一)从TXT文件构建RAG知识库
人工智能·python·大模型·ai编程·ai应用
shhgdxbbb11ffff2 小时前
2026最新2款AI编程工具平替实测:vibe coding全维度深度对比
ai编程
树獭非懒2 小时前
六、Plan-and-Solve智能体:学会三思而后行
人工智能·llm·agent
捧 花2 小时前
YoudaoNoteLM 分层混合 RAG 系统:从多源接入到智能问答的全链路技术架构
架构·llm·agent·rag
吴bug3 小时前
认识 Open-ACE — AI 编程智能体的工作空间
人工智能·ai·ai编程
丹宇码农3 小时前
基于 Top-K Logits 的 LLM 知识蒸馏实战
人工智能·ai·ai编程
掉鱼的猫3 小时前
ReActAgent 使用指南:构建会思考、能行动的 AI Agent
java·llm·agent
浮生望3 小时前
Harness Engineering:给千里马套上缰绳——从 Claude Code 实战看 AI 工程化的终极形态
llm