之前我们已经使用 OpenAI 的嵌入功能为一组内容生成了向量,要实现语义智能搜索功能,可以先把搜索文本转换成向量,然后用余弦(yú xián)相似度计算出两个向量的相似程度,再根据这个相似程度给内容排一下顺序。
在项目里新建一个文件,名字是 semantic-search.mjs,文件顶部导入一个 fs,来自 fs/promises,再从 app.service.mjs 这个模块里面,导入一个 openai,创建 embedding 的时候会用到它上面提供的方法。
文件里声明一个 inputFilePath,它的值是一个文件路径,当前目录 data 下面的 posts_with_embedding.json 。
下面声明一个 data,等于 await,用一下fs.readFile,位置是 inputFilePath,编码是 utf-8 ,另起一行声明一个 posts,用一下JSON.parse 处理一下 data。
命令行
在这个程序里我们可以准备一个命令行工具,提示用户输入要搜索的内容,回车以后可以执行一个任务。这段代码可以让 ChatGPT 帮我们写。“写一个 Node.js 命令行工具,用标准 esm,使用箭头函数,包含注释,程序提示用户输入要搜索的内容,回车以后执行一个任务,完成以后继续提示用户输入要搜索的内容。”
复制一下生成的代码,把它粘贴到我们项目里的 semantic-search.mjs 这个文件里。简单再整理一下,把导入包的代码放到文件的顶部。
这里需要一个 node.js 包,可以在终端给项目安装一下这个包,npm install 要安装的是 readline。
然后可以测试一下这个命令行工具,执行一下 node semantic-search.mjs ,提示输入搜索的内容,输入内容,按下回车,会输出要搜索的内容。
余弦相似度
下面需要再写一个可以计算两个向量余弦相似度用的方法,再让 ChatGPT 试一下。“在 Node.js 项目里实现余弦相似度,使用箭头函数,包含注释。” 这里定义了一个 cosineSimilarity 函数,它可以计算两个向量的相似程度。把这段代码粘贴到我们的项目里。
搜索
然后在要执行的任务这里,也就是 handleInput 这个函数,用 async 标记一下这个函数,在函数里面声明一个 response,等于 await,用一下 openai.createEmbedding 生成嵌入,使用的 model 是 text-embedding-ada-002 ,input 属性的值就是用户输入的搜索文本,这里就是 handleInput 这个函数的参数值,它的值就是用户在命令行输入的要搜索的内容。
下面解构声明一个 embedding 等于 response.data.data[0] 。再根据这个向量,用这个 cosineSimilarity 计算出它跟每一个内容里的向量的相似度。
先在项目里安装一个包,npm install,要安装的是 lodash。在项目文件的顶部,导入一个下划线,来自 lodash 这个包。
在这个执行任务的函数里面,声明一个 results,它的值可以用一下 lodash 里的 map,循环的是 posts 这个数据,提供一个回调函数,当前项目是 item,返回的东西是一个对象,里面先把项目原的东西放进来,再添加一个 similarity ,它的值可以用一下 cosineSimilarity ,第一个向量是 embedding,第二个向量是 item.embedding。
接着调用 sort 这个方法,提供一个回调,两个参数,一个 a,一个b,返回的东西是 a.similarity 减去 b.similarity,也就是根据数据项目的 similarity 的值升序排序,接着调用 reverse ,变成降序排列,然后用一下 slice,取前三个结果,第一个参数值是 0 ,第二个参数值是 3 。继续调用 map 方法,当前项目是 item,索引值是 index,返回的东西是一个字符模板,先是 index + 1 ,表示结果的序号,然后是一个点,再加上 item.title逗号,再加上 item.category。最后调用 join 合并成一个字符串,分隔符号是 '\n' 表示换行符。
在控制台输出这个 results ,在结果之前还有之后可以添加一个换行符。在命令提示的前面也可以再加上一个换行符。
测试
然后在终端,执行一下 node semantic-search.mjs ,会提示请输入要搜索的内容。比如先试一下“我有一组数据要展示”,排在第一的结果是这个包含了 vue.js 和 d3.js 的标题,注意这个搜索并不依赖关键词匹配,它根据的是内容的相似度,比如搜索的是“我有一组数据要展示”,说明用户可能想要通过图表的方式来展示一组数据。所以最相似的内容就是这个用 vue.js 和 d3.js 创建可交互的数据可视化。
再试一下 “我想开发一个能在 macOS 上使用的应用”,返回的这几个结果里面,有 react native,electron 还有 flutter,用这些东西可以开发桌面应用。
再一个 “如何管理项目代码?”,你会发现排在第一位置的是这个 “使用 VSCode 和 Git 进行团队协作开发”,因为 Git 这个工具可以管理项目的代码。
再输入一个 useEffect,返回的第一个结果是如何使用 React Hooks,因为 useEffect 就是 React 这个构架里的一个 Hook。
最后可以再试一下用英文搜索,比如 how to do version control。 在返回的结果里面,排名第一的就是这个包含了 git 的内容标题。