• 作者:老汪软件技巧
  • 发表时间: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)的发展,搜索系统正在逐渐从传统的关键词匹配转向基于向量化的语义搜索。这种方法基于向量空间模型,可以将文本数据转换为高维向量,通过计算相似度来实现搜索。自然语义搜索不仅能够提供更快的查询速度和更高的精度,还能够更好地理解用户的意图,提供更加智能化和个性化的搜索体验。