- 作者:老汪软件技巧
- 发表时间:2024-09-07 17:03
- 浏览量:
前言
在这个 AI 蓬勃发展的时代,它的应用在越来越多的领域中大放异彩;从与 AI 聊天对话到 AI 文生图的功能,这些都只是暂露头角;它还可以极大的提升用户的体验感。
在现代信息检索系统中,搜索功能已成为用户获取信息的关键途径。随着数据量的增加和用户需求的多样化,传统的关键词匹配和模糊搜索方法已无法满足高效、精准的搜索需求。,因此在 AI 的加持下,我们可以实现一个“自然语义搜索”的功能,让搜索结果更精确。
AI 自然语义搜索
何为自然语义搜索?自然语义搜索(Natural Semantic Search)是一种基于自然语言处理(NLP)技术的搜索方式,其目标是让用户能够以接近日常对话的语言形式提出查询请求,并获得更贴近用户意图的搜索结果。
在LLM大模型的世界里,用户提出的所有要求都会被大模型给转换成数学中的向量,任何东西都可以用向量来表达,并且这些嵌入向量可能会有几千个维度;比如猫和狗就会被转换成不同的数学向量。
今天我们就来模拟一下这个高级的用户体验功能。
初始化后端项目并安装依赖
npm init -y
我们可以在项目描述文件(package.json)中手动添加我们需要的依赖 dependencies;
npm install // 安装依赖
添加环境变量 .env 储存 apiKey
出于安全性的考虑,我们应该把apiKey储存到.env文件中;
OpenAI_KEY='xxxxxxxxxxxxxxxxx'
BASE_URL='https://api.gptsapi.net/v1'
dotenv 库是从 .env 文件中加载环境变量到 process.env 中的;因此我们可以通过 process.env.OpenAI_KEY 去拿到这个apiKey。
embedding 把文字转成数学向量
Embedding(嵌入) 在自然语言处理(NLP)领域中是一个非常重要的概念,它指的是将文本数据转换为数值向量的过程。这些向量捕捉了词语、句子甚至是整个文档之间的语义关系,使得计算机可以理解和处理自然语言。
index.mjs
import OpenAI from "openai/index.mjs";
import dotenv from "dotenv";
dotenv.config({
path:".env"
});
const client = new OpenAI({
apiKey: process.env.OpenAI_KEY,
baseURL: process.env.BASE_URL
})
// embedding 文字转成数学向量
const response = await client.embeddings.create({
model: 'text-embedding-ada-002',
input: '百度前端面试题'
})
console.log(response.data[0].embedding);
来看打印的嵌入向量如下所示:
上面中的每个数字都是嵌入向量的一个维度,表示了输入文本“百度前端面试题”的数学表示形式。这个向量可以用于进一步的自然语言处理任务,如相似度计算、聚类分析等。
准备好数据并进行向量化
我们可以去添加 posts.json 文件来模拟一个后端的数据;
[ { "title": "如何使用 Nuxt.js 进行服务器端渲染", "category": "前端开发" }, { "title": "使用 Nest.js 和 TypeScript 构建一个简单的微服务应用", "category": "后端开发" }, { "title": "如何在 Vue.js 中使用 Vuetify 实现 Material Design 风格", "category": "前端开发" }, { "title": "如何使用 Nuxt.js 和 Firebase 实现服务器端渲染的无后端应用", "category": "前端开发" }, { "title": "使用 Nest.js 和 TypeORM 构建一个简单的数据驱动的 RESTful API", "category": "后端开发" }, { "title": "如何使用 Vue.js 和 Electron 开发桌面应用程序", "category": "前端开发" }, { "title": "使用 Nuxt.js 和 Storybook 构建可视化组件库", "category": "前端开发" }, { "title": "如何使用 Nest.js 和 Passport 实现用户认证", "category": "后端开发" }, { "title": "如何使用 Vue.js 和 D3.js 创建可交互的数据可视化", "category": "前端开发" }, { "title": "使用 Nest.js 和 GraphQL 构建一个简单的 GraphQL 服务", "category": "后端开发" }, { "title": "如何在 React 中实现无限滚动", "category": "前端开发" }, { "title": "使用 Flask 和 Python 构建 RESTful API", "category": "后端开发" }, { "title": "如何使用 React Native 开发跨平台移动应用", "category": "移动开发" }, { "title": "掌握 Pandas 中的分组和聚合操作", "category": "数据科学" }, { "title": "如何在 Vue.js 中使用 Vuex 进行状态管理", "category": "前端开发" }, { "title": "使用 Django 和 Python 构建一个简单的博客应用", "category": "后端开发" }, { "title": "如何使用 React Hooks 构建可复用的组件", "category": "前端开发" }, { "title": "如何使用 TensorFlow 进行图像分类任务", "category": "数据科学" }, { "title": "如何使用 Flutter 开发一个简单的计数器应用", "category": "移动开发" }, { "title": "使用 Node.js 和 Express 构建一个简单的 RESTful API", "category": "后端开发" }, { "title": "如何使用 D3.js 创建可交互的数据可视化", "category": "前端开发" }, { "title": "如何使用 Scikit-learn 进行机器学习任务", "category": "数据科学" }, { "title": "如何使用 React Router 实现客户端路由", "category": "前端开发" }, { "title": "使用 Ruby on Rails 构建一个简单的电商网站", "category": "后端开发" }, { "title": "如何使用 Kotlin 和 Android Studio 开发一个简单的 Todo 应用", "category": "移动开发" }, { "title": "如何使用 PyTorch 进行深度学习任务", "category": "数据科学" }, { "title": "使用 VSCode 和 Git 进行团队协作开发", "category": "开发工具" }, { "title": "如何使用 Insomnia 进行 API 接口测试", "category": "开发工具" }, { "title": "如何使用 TypeScript 编写高质量的 JavaScript 代码", "category": "前端开发" }, { "title": "如何使用 CSS 实现网页响应式布局", "category": "前端开发" }, { "title": "使用 JavaScript 实现一个简单的计算器应用", "category": "前端开发" }, { "title": "使用 Next.js 和 Tailwind CSS 构建一个简单的博客应用", "category": "前端开发" }, { "title": "如何使用 Vue.js 和 Tailwind CSS 创建响应式布局", "category": "前端开发" }, { "title": "如何在 React 中使用 Tailwind CSS 实现 Material Design 风格", "category": "前端开发" }, { "title": "使用 Tailwind CSS 和 Alpine.js 构建一个简单的交互式表单", "category": "前端开发" } ]
我们还需要把这些数据进行一个向量化;并把嵌入向量导出到一个文件 posts_with_embedding.json 中;
create-embedding.mjs
import fs from 'fs/promises' // promisify
import OpenAI from "openai/index.mjs";
import dotenv from "dotenv";
dotenv.config({
path:".env"
});
const client = new OpenAI({
apiKey: process.env.OpenAI_KEY,
baseURL: process.env.BASE_URL
})
const inputFilePath = './data/posts.json'
const outputFilePath = './data/posts_with_embedding.json'
const data = await fs.readFile(inputFilePath, 'utf8')
const posts = JSON.parse(data)
console.log(posts);
const postsWithEmbedding = []
for (const { title, category } of posts){
const response = await client.embeddings.create({
model: 'text-embedding-ada-002',
input: `标题:${title} 分类:${category}`
})
postsWithEmbedding.push({
title,
category,
embedding: response.data[0].embedding
})
}
await fs.writeFile(outputFilePath, JSON.stringify(postsWithEmbedding))
如下所示,每一项都多加了它的数学向量值;
将用户输入进行向量的相似度计算
接下来,我们可以利用向量的余弦相似度来拿到最匹配的结果;
search.mjs
import readline from "readline";
import fs from 'fs/promises'; // 引入 fs 模块的 promises 版本,用于文件操作
import OpenAI from "openai/index.mjs";
import dotenv from "dotenv";
dotenv.config({
path: ".env"
});
// 定义输入文件路径
const inputFilePath = './data/posts_with_embedding.json';
// 读取文件并解析 JSON 数据
const data = await fs.readFile(inputFilePath, 'utf8');
const posts = JSON.parse(data);
/**
* 计算两个向量的余弦相似度
* @param {Array } v1 - 第一个向量
* @param {Array } v2 - 第二个向量
* @returns {number} - 余弦相似度值
*/
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));
// 计算余弦相似度
const similarity = dotProduct / (lengthV1 * lengthV2);
return similarity;
};
// 创建 OpenAI 客户端实例
const client = new OpenAI({
apiKey: process.env.OpenAI_KEY,
baseURL: process.env.BASE_URL
});
// 创建 readline 接口实例
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
/**
* 处理用户输入,计算输入与帖子的余弦相似度,并输出最相似的三个帖子
* @param {string} input - 用户输入的内容
*/
const handleInput = async (input) => {
console.log(input);
// 使用 OpenAI API 创建输入的嵌入向量
const response = await client.embeddings.create({
model: 'text-embedding-ada-002',
input: input
});
const { embedding } = response.data[0];
// 计算每个帖子与输入的余弦相似度
const results = posts.map(item => ({
...item,
similarity: cosineSimilarity(embedding, item.embedding)
}))
// 对结果进行排序,最相似的排在前面
.sort((a, b) => a.similarity - b.similarity)
// 取前三个结果
.reverse()
.slice(0, 3)
// 格式化输出结果
.map((item, index) => `${index + 1},${item.title},${item.category}`)
.join('\n');
console.log(`\n${results}\n`);
// 继续询问用户输入
rl.question('\n请输入要搜索的内容', handleInput)
};
// 启动程序,询问用户输入
rl.question('\n请输入要搜索的内容', handleInput);
余弦相似度越大,就说明这两个向量的相似度越高;我们可以把相似度高的结果输出。
效果如下:
可以看到,我们输入vue,自然语言搜索给我们返回了除了vue外的Nuxt.js;这恰恰说明,自然语言搜索并不是无脑地进行字符串匹配,它可以去语义化地理解我们的需求。
小结
随着大模型(如 GPT-4、BERT)的发展,搜索系统正在逐渐从传统的关键词匹配转向基于向量化的语义搜索。这种方法基于向量空间模型,可以将文本数据转换为高维向量,通过计算相似度来实现搜索。自然语义搜索不仅能够提供更快的查询速度和更高的精度,还能够更好地理解用户的意图,提供更加智能化和个性化的搜索体验。