本文将之前的文章,实现一个场景的实战应用,包含代码等内容。利用纯前端实现增强的列表搜索,抛弃字符串匹配,目标是使用番茄关键字可以搜索到西红柿
1 准备工作
1.1 了解llm和web开发
web端的ai开发参考 前端大模型入门:使用Transformers.js手搓纯网页版RAG(二)前端大模型入门:使用Transformers.js实现纯网页版RAG(一)
前端框架使用的vue3+antdv,最好是懂相关代码,读懂即可。
1.2 开发环境和工具
- node20+
- vite
1.3 工程准备
// init.sh // 创建vite vue-ts项目 yarn create vite test-ra-list --template vue-ts // 进入工程目录 cd test-ra-list // 安装依赖 yarn add ant-design-vue @xenova/transformers
1.4 本地模型目录准备
在public下面创建一个models目录,然后创建各个模型的子目录,以便后续将模型文件放入其中
1.5 下载模型文件
在hf-mirror找到想用模型,本文用到的在Xenova/bge-base-zh-v1.5 at main (hf-mirror.com),点击各个文件的下载图标,然后存储在对应目录下
编辑
下载模型文件,默认是quantized,除非你配置加载高精模型,也可以三个都下载,记得在1.4模型名的目录下新建一个onnx目录
编辑
最后public目录如下图所示
1.6 创建模拟数据
在public目录下创建一个data.json
[{"name":"PC1","id":"5F62AD98-9BAF-0B46-A506-D8EF3749D325"},{"name":"PC2","id":"58CE02BF-6F95-3F4C-9BF6-450E355BBD94"},{"name":"西红柿","id":"8FF8BC68-6BF3-0A4C-AD87-668C1CED3234"},{"name":"aaa11","id":"E6B61EFC-9730-4945-84C8-0C1FCF068AB6"},{"name":"地瓜-0","id":"3B26D363-6720-B241-AB1A-AE7C3BB1A989"},{"name":"地瓜-1","id":"A79DE23B-6A53-354A-90EA-3BAF90E43629"},{"name":"西红柿-10","id":"E3C781BF-F6ED-364E-923C-B9CA3C38BEA1"},{"name":"洋芋-100","id":"81E42720-3C18-9C4F-A302-D86C6AF51989"},{"name":"西红柿-101","id":"A98E902D-3ECB-A748-A3E6-2F4C2D36FD55"},{"name":"洋芋-102","id":"6B02AC77-55D4-7C40-98A3-383D52D72929"},{"name":"番茄-103","id":"D6E45494-BD47-5848-8492-287437155A3D"},{"name":"马铃薯-104","id":"7C4CB80B-6C0D-EC4A-A5BD-52E65D8EC2FF"},{"name":"土豆-104","id":"1C3829C0-8356-024E-AF90-9BC456A78E29"},{"name":"马铃薯-105","id":"5560C41C-46B2-BC44-9141-92E83B62D5C8"},{"name":"地瓜-106","id":"20598CEC-5E31-3F49-A578-A6F026018CC0"},{"name":"红薯-107","id":"E1061811-0886-0840-B387-A1321DA5212D"},{"name":"马铃薯-108","id":"D302EF74-0402-1F43-A4FD-FBF2CE852B5E"},{"name":"红薯-109","id":"608D7A1C-C265-9A4B-99D6-A08EBBDD08EF"},{"name":"番茄-11","id":"A19882CE-2B37-D64C-95E2-A8BC769D9A06"},{"name":"洋芋-110","id":"6D80D92B-540B-2A4D-AF2F-A15C8B04EB3F"},{"name":"番茄-111","id":"6F229077-AF25-D241-BEB4-0E53852EAF61"},{"name":"马铃薯-12","id":"A108EDCD-42D0-0B4E-9691-62FB8572ECF8"},{"name":"地瓜-13","id":"FB31B7D1-4CD3-F44C-9ED4-C659EDB58B25"}]
2 实现方法
首先分析和分解任务:1 列表呈现数据;2 高级搜索功能
2.1 数据加载和列表数据展示
这部分使用antdv的table可以很快速的展示数据,数据加载就使用fetch即可
type RawInfo = { name: string; id: string; }; const loading = ref(false); const items = ref<RawInfo[]>([]); onMounted(() => { fetch("data.json") .then((res) => res.json()) .then((list) => (items.value = list)); }); const columns = [ { title: "序号", dataIndex: "index", key: "index", customRender: (e: { index: number }) => { return h("span", {}, e.index + 1); }, width: 84, }, { title: "名称", dataIndex: "name", key: "name", } ] as ColumnsType<any>;
<Table :loading="loading" :dataSource="items" :columns="columns" />
2.2 搜索数据显示
为了动态显示搜索结果和原始结果,使用一个searchKey来切换显示的数据源。
const searchKey = ref(""); const showItems = computed(() => { return searchKey.value ? result.value : items.value; }); const search = async (e: string) => { searchKey.value = e || ""; if (!e) { return; } // 待完成搜索 };
<InputSearch placeholder="请输入搜索内容" @search="search" /> <Table :loading="loading" :dataSource="showItems" :columns="columns" />
2.3 模型参数准备
- 模型加载路径即为之前创建的public下的/models目录
- topK表示结果最多显示10个
- 使用minScore指定最低的相似度
import { cos_sim, env, pipeline } from "@xenova/transformers"; env.remoteHost = "/models/"; const topK = 10; const minScore = 0.6; const pipe = pipeline("feature-extraction", "bge-base-zh-v1.5", { progress_callback: (d: any) => { console.log(d); }, });
2.4 向量数组构建
深度搜索的核心就是高位空间的相似度(距离)匹配,所以需要将数据全部进行Emebdding
const buildVector = async () => { if (!items.value.length) return; const list = items.value; loading.value = true; vectors.length = list.length; await nextTick(); const embedding = await pipe; const questions = list.map((item) => item.name); const output = (await embedding(questions, { pooling: "mean", normalize: true, })) as any; console.log(output); questions.forEach((q, i) => { vectors[i] = output[i]; }); loading.value = false; };
2.5 相似度计算
将关键词/字进行向量化,然后依次计算相似度,而不是使用子字符串/包含关系的匹配。
const embedding = await pipe; const [vector] = await embedding([e], { pooling: "mean", normalize: true, }); if (!vectors.length) { await buildVector(); } const scores = vectors.map((q, i) => { return { score: cos_sim(vector.data, vectors[i].data), index: i, }; });
2.6 结果筛选
最后,根据匹配度排序,过滤掉相似度过低的,再取相似度最高的topK项
scores.sort((a, b) => b.score - a.score); console.log(scores); result.value = scores .filter((e) => e.score > minScore) .slice(0, topK) .map((s) => items.value[s.index]); console.log( `搜索到${result.value.length}条记录:topK=${topK} minScore=${minScore}` );
3 实际效果
3.1 番茄 - 可搜索到西红柿
编辑
3.2 红薯-可搜索到地瓜
编辑
4 待改进点
4.1 模型精度
目前使用的是最小的模型,以便于都能体验,效果会有一点差,但整体结果还算理想
4.2 最低相似度和topK控制
这两个参数对结果的影响也不小,实际上我想去掉相似度过滤,而是直接选出topK可能好一点
4.3 嵌入改进 - 非阻塞
目前在初次计算向量组(列表元素向量)是比较耗资源的,会造成页面卡顿,这部分可考虑在worker或者做成单条异步运算,而不是一次性计算出所有条目的嵌入向量